-
Notifications
You must be signed in to change notification settings - Fork 118
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Instrument test console log parser (#1824)
Fixes #1800 Documentation as javadoc in [Parser.kt](https://github.com/Flank/flank/blob/1800-parsing-corellium-logs/corellium/logs/src/main/kotlin/flank/corellium/logs/Parser.kt) file
- Loading branch information
1 parent
da06aa9
commit 98efc47
Showing
6 changed files
with
2,029 additions
and
1 deletion.
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile | ||
|
||
plugins { | ||
kotlin(Plugins.Kotlin.PLUGIN_JVM) | ||
kotlin(Plugins.Kotlin.PLUGIN_SERIALIZATION) version Versions.KOTLIN | ||
} | ||
|
||
repositories { | ||
mavenCentral() | ||
maven(url = "https://kotlin.bintray.com/kotlinx") | ||
} | ||
|
||
tasks.withType<KotlinCompile> { kotlinOptions.jvmTarget = "1.8" } | ||
|
||
dependencies { | ||
implementation(Dependencies.KOTLIN_COROUTINES_CORE) | ||
testImplementation(Dependencies.JUNIT) | ||
} |
144 changes: 144 additions & 0 deletions
144
corellium/log/src/main/kotlin/flank/corellium/log/Internal.kt
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,144 @@ | ||
package flank.corellium.log | ||
|
||
import kotlinx.coroutines.flow.Flow | ||
import kotlinx.coroutines.flow.map | ||
import kotlinx.coroutines.flow.transform | ||
|
||
// Stage 1 ============================================ | ||
/** | ||
* Group instrumentation output logs | ||
* @receiver Flow of lines | ||
* @return Flow of groups where the last line always contains status code | ||
*/ | ||
internal fun Flow<String>.groupLines(): Flow<List<Line>> { | ||
val accumulator = mutableListOf<Line>() | ||
return transform { line -> | ||
val (prefix, text) = line.parsePrefix() | ||
accumulator += Line(prefix, text) | ||
when (prefix) { | ||
Type.StatusCode.text, | ||
Type.Code.text -> { | ||
emit(accumulator.toList()) | ||
accumulator.clear() | ||
} | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Parsed line of instrumentation output. For example: | ||
* ``` | ||
* INSTRUMENTATION_STATUS: test=ignoredTestWithIgnore | ||
* ``` | ||
* @property prefix `"INSTRUMENTATION_STATUS: "` | ||
* @property text `"test=ignoredTestWithIgnore"` | ||
*/ | ||
internal data class Line( | ||
val prefix: String?, | ||
val text: String, | ||
) | ||
|
||
private fun String.parsePrefix(): Pair<String?, String> { | ||
val prefix = Type.values().firstOrNull { startsWith(it.text) }?.text | ||
return prefix to (prefix?.let { removePrefix(it) } ?: this) | ||
} | ||
|
||
private enum class Type(val text: String) { | ||
Status("INSTRUMENTATION_STATUS: "), | ||
StatusCode("INSTRUMENTATION_STATUS_CODE: "), | ||
Result("INSTRUMENTATION_RESULT: "), | ||
Code("INSTRUMENTATION_CODE: "), | ||
} | ||
|
||
// Stage 2 ============================================ | ||
/** | ||
* Parse previously grouped lines into chunks | ||
* @receiver [Flow] of [Line] groups | ||
* @return [Flow] of [Chunk] | ||
*/ | ||
internal fun Flow<List<Line>>.parseChunks(): Flow<Chunk> = map { group -> | ||
val reversed = group.reversed().toMutableList() | ||
val code = reversed.removeFirst() | ||
val linesAccumulator = mutableListOf<String>() | ||
val map = mutableMapOf<String, List<String>>() | ||
reversed.forEach { line -> | ||
if (line.prefix == null) | ||
linesAccumulator += line.text | ||
else | ||
linesAccumulator.apply { | ||
val (key, text) = line.text.split("=", limit = 2) | ||
add(text) | ||
map[key] = reversed() | ||
clear() | ||
} | ||
} | ||
Chunk(code.prefix!!, code.text.toInt(), map) | ||
} | ||
|
||
/** | ||
* The structured representation of instrumentation output lines followed by result code. | ||
* @property type The prefix of status code line: [INSTRUMENTATION_STATUS_CODE | INSTRUMENTATION_CODE] | ||
* @property code The result code. | ||
* @property map The properties for the specific group of lines. | ||
*/ | ||
internal data class Chunk( | ||
val type: String, | ||
val code: Int, | ||
val map: Map<String, List<String>>, | ||
val timestamp: Long = System.currentTimeMillis(), | ||
) | ||
|
||
// Stage 3 ============================================ | ||
|
||
internal fun Flow<Chunk>.parseStatusResult(): Flow<Instrument> { | ||
var prev = Chunk( | ||
type = "", | ||
code = 0, | ||
map = mapOf("current" to listOf("0")) | ||
) | ||
|
||
return transform { next -> | ||
when (next.type) { | ||
|
||
// Handling the regular chunk which is representing the half of Status. | ||
Type.StatusCode.text -> when { | ||
prev.id == next.id -> emit(createStatus(prev, next)) | ||
prev.id < next.id -> prev = next | ||
else -> throw IllegalArgumentException("Inconsistent stream of chunks.\nexpected pair for: $prev\nbut was $next") | ||
} | ||
|
||
// Handling the final chunk which is representing the Result. | ||
Type.Code.text -> emit(createResult(next)) | ||
|
||
else -> throw IllegalArgumentException("Unknown type of Chunk: ${next.type}") | ||
} | ||
} | ||
} | ||
|
||
private val Chunk.id: Int get() = map.getValue("current").first().toInt() | ||
|
||
private fun createStatus(first: Chunk, second: Chunk) = Instrument.Status( | ||
code = second.code, | ||
startTime = first.timestamp, | ||
endTime = second.timestamp, | ||
details = (first.map + second.map).mapValues { (key, value) -> | ||
when (key) { | ||
"id", | ||
"test", | ||
"class", | ||
-> value.first() | ||
|
||
"current", | ||
"numTests", | ||
-> value.first().toInt() | ||
|
||
else -> value | ||
} | ||
} | ||
) | ||
|
||
private fun createResult(chunk: Chunk) = Instrument.Result( | ||
code = chunk.code, | ||
details = chunk.map, | ||
time = chunk.timestamp | ||
) |
73 changes: 73 additions & 0 deletions
73
corellium/log/src/main/kotlin/flank/corellium/log/Parser.kt
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,73 @@ | ||
package flank.corellium.log | ||
|
||
import kotlinx.coroutines.flow.Flow | ||
|
||
/** | ||
* Parse instrument test logs into structures. | ||
* This parser requires a clean logs only from the single command execution. | ||
* | ||
* @receiver The [Flow] of [String] lines from console output produced by "am instrument -r -w" command. | ||
* @return The [Flow] of [Instrument] structures. Only the last element of flow is [Instrument.Result] | ||
*/ | ||
fun Flow<String>.parseAdbInstrumentLog(): Flow<Instrument> = this | ||
.groupLines() | ||
.parseChunks() | ||
.parseStatusResult() | ||
|
||
sealed class Instrument { | ||
/** | ||
* Representation of the following pair of two status chunks: | ||
* ``` | ||
* INSTRUMENTATION_STATUS: class=com.example.test_app.InstrumentedTest | ||
* INSTRUMENTATION_STATUS: current=1 | ||
* INSTRUMENTATION_STATUS: id=AndroidJUnitRunner | ||
* INSTRUMENTATION_STATUS: numtests=3 | ||
* INSTRUMENTATION_STATUS: stream= | ||
* com.example.test_app.InstrumentedTest: | ||
* INSTRUMENTATION_STATUS: test=ignoredTestWithIgnore | ||
* INSTRUMENTATION_STATUS_CODE: 1 | ||
* INSTRUMENTATION_STATUS: class=com.example.test_app.InstrumentedTest | ||
* INSTRUMENTATION_STATUS: current=1 | ||
* INSTRUMENTATION_STATUS: id=AndroidJUnitRunner | ||
* INSTRUMENTATION_STATUS: numtests=3 | ||
* INSTRUMENTATION_STATUS: stream= | ||
* com.example.test_app.InstrumentedTest: | ||
* INSTRUMENTATION_STATUS: test=ignoredTestWithIgnore | ||
* INSTRUMENTATION_STATUS_CODE: -3 | ||
* ``` | ||
* | ||
* @property code The value of INSTRUMENTATION_STATUS_CODE of the second chunk | ||
* @property startTime The time of creation the first chunk of status. | ||
* @property endTime The time of creation the second chunk of status. | ||
* @property details The summary details of both chunks. | ||
*/ | ||
class Status( | ||
val code: Int, | ||
val startTime: Long, | ||
val endTime: Long, | ||
val details: Map<String, Any> | ||
) : Instrument() | ||
|
||
/** | ||
* Representation of the final structure of instrument test logs: | ||
* ``` | ||
* INSTRUMENTATION_RESULT: stream= | ||
* | ||
* Time: 2.076 | ||
* | ||
* OK (2 test) | ||
* | ||
* | ||
* INSTRUMENTATION_CODE: -1 | ||
* ``` | ||
* | ||
* @property code The value of INSTRUMENTATION_CODE | ||
* @property time The time of creation the result chunk. | ||
* @property details The details recorded for the result (Perhaps only a "stream" value). | ||
*/ | ||
class Result( | ||
val code: Int, | ||
val time: Long, | ||
val details: Map<String, Any> | ||
) : Instrument() | ||
} |
67 changes: 67 additions & 0 deletions
67
corellium/log/src/test/kotlin/flank/corellium/log/InternalTest.kt
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,67 @@ | ||
package flank.corellium.log | ||
|
||
import kotlinx.coroutines.flow.Flow | ||
import kotlinx.coroutines.flow.asFlow | ||
import kotlinx.coroutines.flow.toList | ||
import kotlinx.coroutines.runBlocking | ||
import org.junit.Assert | ||
import org.junit.Test | ||
|
||
class InternalTest { | ||
|
||
@Test | ||
fun groupLinesTest() { | ||
val lines = runBlocking { | ||
flowLogs() | ||
.groupLines() | ||
.toList() | ||
} | ||
|
||
// 2 * 29 statuses + 1 result | ||
Assert.assertEquals(59, lines.size) | ||
} | ||
|
||
@Test | ||
fun parseChunksTest() { | ||
val chunks = runBlocking { | ||
flowLogs() | ||
.groupLines() | ||
.parseChunks() | ||
.toList() | ||
} | ||
|
||
// 2 * 29 statuses + 1 result | ||
Assert.assertEquals(59, chunks.size) | ||
} | ||
|
||
@Test | ||
fun parseInstrumentStatus() { | ||
val statusResults = runBlocking { | ||
flowLogs() | ||
.groupLines() | ||
.parseChunks() | ||
.parseStatusResult() | ||
.toList() | ||
} | ||
|
||
// 29 statuses + 1 result | ||
Assert.assertEquals(30, statusResults.size) | ||
|
||
Assert.assertTrue( | ||
"All items except the last one must be Instrument.Status", | ||
statusResults.dropLast(1).all { it is Instrument.Status } | ||
) | ||
|
||
Assert.assertTrue( | ||
"Last item must be Instrument.Result", | ||
statusResults.last() is Instrument.Result | ||
) | ||
} | ||
} | ||
|
||
private fun flowLogs(): Flow<String> = | ||
Unit.javaClass.classLoader | ||
.getResourceAsStream("example_android_logs.txt")!! | ||
.bufferedReader() | ||
.lineSequence() | ||
.asFlow() |
Oops, something went wrong.