Skip to content

Commit 1ce78ca

Browse files
committed
Add single & multi line formats for executions status printing
1 parent b65ae75 commit 1ce78ca

16 files changed

Lines changed: 270 additions & 100 deletions

File tree

test_runner/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,8 @@ dependencies {
215215
testImplementation(Libs.MOCKK)
216216

217217
implementation(Libs.COMMON_TEXT)
218+
219+
implementation(Libs.JANSI)
218220
}
219221

220222
// Fix Exception in thread "main" java.lang.NoSuchMethodError: com.google.common.hash.Hashing.crc32c()Lcom/google/common/hash/HashFunction;

test_runner/buildSrc/src/main/kotlin/Deps.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ object Versions {
7777

7878
//https://commons.apache.org/proper/commons-text/
7979
const val COMMON_TEXT = "1.8"
80+
81+
//https://github.com/fusesource/jansi/releases
82+
const val JANSI = "1.18"
8083
}
8184

8285
object Libs {
@@ -112,6 +115,8 @@ object Libs {
112115

113116
const val KOTLIN_LOGGING = "io.github.microutils:kotlin-logging:${Versions.KOTLIN_LOGGING}"
114117

118+
const val JANSI = "org.fusesource.jansi:jansi:${Versions.JANSI}"
119+
115120
//region Plugins
116121
const val DETEKT_FORMATTING = "io.gitlab.arturbosch.detekt:detekt-formatting:${Versions.DETEKT}"
117122
//endregion

test_runner/src/main/kotlin/ftl/Main.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,17 @@ class Main : Runnable {
5252
println()
5353
CommandLine(Main()).execute(*args)
5454
}
55+
// runBlocking {
56+
// AnsiConsole.systemInstall()
57+
// println("1")
58+
// println("2")
59+
// delay(500)
60+
// print(Ansi.ansi().cursorUpLine().eraseLine())
61+
// print(Ansi.ansi().cursorUpLine().eraseLine())
62+
// delay(500)
63+
// println("3")
64+
// delay(500)
65+
// }
5566
}
5667
}
5768
}

test_runner/src/main/kotlin/ftl/args/IArgs.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package ftl.args
22

33
import ftl.args.yml.FlankYmlParams
4+
import ftl.run.status.OutputStyle
45
import ftl.util.timeoutToMils
56

67
// Properties common to both Android and iOS
@@ -39,6 +40,7 @@ interface IArgs {
3940
val useLegacyJUnitResult: Boolean get() = false
4041
val ignoreFailedTests: Boolean
4142
val keepFilePath: Boolean
43+
val outputStyle: OutputStyle? get() = OutputStyle.Verbose
4244

4345
fun useLocalResultDir() = localResultDir != FlankYmlParams.defaultLocalResultsDir
4446

@@ -47,3 +49,9 @@ interface IArgs {
4749
val AVAILABLE_SHARD_COUNT_RANGE = 1..50
4850
}
4951
}
52+
53+
fun IArgs.outputStyle() = outputStyle ?: let {
54+
if (flakyTestAttempts > 0 || (!disableSharding && maxTestShards > 0))
55+
OutputStyle.Multi else
56+
OutputStyle.Verbose
57+
}

test_runner/src/main/kotlin/ftl/json/MatrixMap.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,6 @@ class MatrixMap(
4343
}
4444
}
4545

46-
fun List<TestMatrix>.update(matrixMap: MatrixMap) = forEach { matrix ->
46+
fun Iterable<TestMatrix>.update(matrixMap: MatrixMap) = forEach { matrix ->
4747
matrixMap.map[matrix.testMatrixId]?.update(matrix)
4848
}

test_runner/src/main/kotlin/ftl/run/common/PollMatrices.kt

Lines changed: 25 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -2,43 +2,40 @@
22

33
package ftl.run.common
44

5-
import com.google.api.services.testing.model.Environment
6-
import com.google.api.services.testing.model.TestExecution
75
import com.google.api.services.testing.model.TestMatrix
86
import ftl.args.IArgs
97
import ftl.gc.GcTestMatrix
10-
import ftl.util.StopWatch
11-
import ftl.util.StopWatchMatrix
12-
import ftl.util.isCompleted
13-
import kotlinx.coroutines.Dispatchers
14-
import kotlinx.coroutines.async
15-
import kotlinx.coroutines.awaitAll
8+
import ftl.run.status.TestMatrixStatusPrinter
9+
import ftl.util.MatrixState
1610
import kotlinx.coroutines.coroutineScope
1711
import kotlinx.coroutines.delay
1812
import kotlinx.coroutines.flow.Flow
13+
import kotlinx.coroutines.flow.asFlow
14+
import kotlinx.coroutines.flow.flatMapMerge
1915
import kotlinx.coroutines.flow.flow
16+
import kotlinx.coroutines.flow.fold
2017
import kotlinx.coroutines.flow.onEach
21-
import kotlinx.coroutines.flow.reduce
2218

2319
suspend fun pollMatrices(
2420
testMatricesIds: Iterable<String>,
2521
args: IArgs,
26-
stopWatch: StopWatch = StopWatch()
27-
): List<TestMatrix> = coroutineScope {
28-
stopWatch.start()
29-
testMatricesIds.map { testMatrixId ->
30-
async(Dispatchers.IO) {
31-
matrixChangesFlow(
32-
testMatrixId = testMatrixId,
33-
projectId = args.project
34-
).printChanges(
35-
stopWatch = stopWatch,
36-
args = args
37-
).reduce { _, latest ->
38-
latest
39-
}
40-
}
41-
}.awaitAll()
22+
printMatrixStatus: (TestMatrix) -> Unit = TestMatrixStatusPrinter(
23+
args = args,
24+
testMatricesIds = testMatricesIds
25+
)
26+
): Collection<TestMatrix> = coroutineScope {
27+
testMatricesIds.asFlow().flatMapMerge { testMatrixId ->
28+
matrixChangesFlow(
29+
testMatrixId = testMatrixId,
30+
projectId = args.project
31+
)
32+
}.onEach {
33+
printMatrixStatus(it)
34+
}.fold(emptyMap<String, TestMatrix>()) { matrices, next ->
35+
matrices + (next.testMatrixId to next)
36+
}.values.also {
37+
println()
38+
}
4239
}
4340

4441
private fun matrixChangesFlow(
@@ -52,63 +49,6 @@ private fun matrixChangesFlow(
5249
}
5350
}
5451

55-
private fun Flow<TestMatrix>.printChanges(
56-
stopWatch: StopWatch,
57-
args: IArgs,
58-
cache: MutableMap<String, ExecutionStatus> = mutableMapOf()
59-
) = onEach { matrix: TestMatrix ->
60-
matrix.testExecutions.map { execution ->
61-
printExecutionStatus(
62-
watch = StopWatchMatrix(stopWatch, execution.formatName(args)),
63-
previous = cache[execution.id] ?: ExecutionStatus(),
64-
current = execution.getDeviceStatus().also {
65-
cache[execution.id] = it
66-
}
67-
)
68-
}
69-
if (matrix.isCompleted) StopWatchMatrix(
70-
stopwatch = stopWatch,
71-
matrixId = matrix.testMatrixId
72-
).puts(matrix.state)
73-
}
74-
75-
private fun printExecutionStatus(
76-
previous: ExecutionStatus,
77-
current: ExecutionStatus,
78-
watch: StopWatchMatrix
79-
) {
80-
if (current.error != previous.error && current.error != null) {
81-
watch.puts("Error: ${current.error}")
82-
}
83-
current.progress.minus(previous.progress).forEach { message ->
84-
watch.puts(message)
85-
}
86-
if (current.state != previous.state) {
87-
watch.puts(current.state)
88-
}
89-
}
90-
91-
private fun TestExecution.formatName(args: IArgs): String {
92-
val matrixExecutionId = id.split("_")
93-
val matrixId = matrixExecutionId.first()
94-
val executionId = matrixExecutionId.takeIf { args.flakyTestAttempts > 0 }?.getOrNull(1)?.let { " $it" } ?: ""
95-
val env: Environment? = environment
96-
val device = env?.androidDevice?.androidModelId ?: env?.iosDevice?.iosModelId
97-
val deviceVersion = env?.androidDevice?.androidVersionId ?: env?.iosDevice?.iosVersionId
98-
val shard = shard?.takeUnless { args.disableSharding }?.run { " shard-${(shardIndex ?: 0)}" } ?: ""
99-
return "$matrixId $device-$deviceVersion$shard$executionId"
100-
}
101-
102-
private fun TestExecution.getDeviceStatus() = ExecutionStatus(
103-
testExecutionId = id,
104-
state = state,
105-
error = testDetails?.errorMessage,
106-
progress = testDetails?.progressMessages ?: emptyList()
107-
)
108-
109-
private data class ExecutionStatus(
110-
val testExecutionId: String = "",
111-
val state: String = "",
112-
val error: String? = null,
113-
val progress: List<String> = emptyList()
114-
)
52+
private val TestMatrix.isCompleted: Boolean
53+
get() = MatrixState.completed(state) &&
54+
testExecutions?.all { MatrixState.completed(it.state) } ?: true
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package ftl.run.status
2+
3+
import ftl.config.FtlConstants
4+
5+
data class ExecutionStatus(
6+
val state: String = "",
7+
val error: String? = null,
8+
val progress: List<String> = emptyList()
9+
) {
10+
data class Change(
11+
val name: String,
12+
val previous: ExecutionStatus,
13+
val current: ExecutionStatus,
14+
val time: String
15+
)
16+
data class View(
17+
val time: String,
18+
val id: String,
19+
val status: String
20+
) {
21+
override fun toString() = "${FtlConstants.indent}$time $id $status"
22+
}
23+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package ftl.run.status
2+
3+
import com.google.api.services.testing.model.Environment
4+
import com.google.api.services.testing.model.TestExecution
5+
import ftl.args.IArgs
6+
7+
class ExecutionStatusListPrinter(
8+
private val args: IArgs,
9+
private val executionsCache: LinkedHashMap<String, ExecutionStatus> = LinkedHashMap(),
10+
private val printExecutionStatues: (List<ExecutionStatus.Change>) -> Unit = createExecutionStatusPrinter(args)
11+
) : (String, List<TestExecution>?) -> Unit {
12+
override fun invoke(
13+
time: String,
14+
executions: List<TestExecution>?
15+
) = getChanges(time, executions).let(printExecutionStatues)
16+
17+
private fun getChanges(
18+
time: String,
19+
executions: List<TestExecution>?
20+
): List<ExecutionStatus.Change> = executions?.map { execution ->
21+
ExecutionStatus.Change(
22+
name = execution.formatName(args),
23+
previous = executionsCache[execution.id] ?: ExecutionStatus(),
24+
current = execution.executionStatus().also { status ->
25+
executionsCache[execution.id] = status
26+
},
27+
time = time
28+
)
29+
} ?: emptyList()
30+
}
31+
32+
private fun TestExecution.formatName(args: IArgs): String {
33+
val matrixExecutionId = id.split("_")
34+
val matrixId = matrixExecutionId.first()
35+
val executionId = matrixExecutionId.takeIf { args.flakyTestAttempts > 0 }?.getOrNull(1)?.let { " $it" } ?: ""
36+
val env: Environment? = environment
37+
val device = env?.androidDevice?.androidModelId ?: env?.iosDevice?.iosModelId
38+
val deviceVersion = env?.androidDevice?.androidVersionId ?: env?.iosDevice?.iosVersionId
39+
val shard = shard?.takeUnless { args.disableSharding }?.run { " shard-${(shardIndex ?: 0)}" } ?: ""
40+
return "$matrixId $device-$deviceVersion$shard$executionId"
41+
}
42+
43+
private fun TestExecution.executionStatus() = ExecutionStatus(
44+
state = state,
45+
error = testDetails?.errorMessage,
46+
progress = testDetails?.progressMessages ?: emptyList()
47+
)
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package ftl.run.status
2+
3+
import com.google.common.annotations.VisibleForTesting
4+
import ftl.args.IArgs
5+
import ftl.args.outputStyle
6+
import ftl.config.FtlConstants
7+
import org.fusesource.jansi.Ansi
8+
import org.fusesource.jansi.AnsiConsole
9+
10+
fun createExecutionStatusPrinter(
11+
args: IArgs
12+
): (List<ExecutionStatus.Change>) -> Unit = when (args.outputStyle()) {
13+
OutputStyle.Multi -> MultiLinePrinter()
14+
OutputStyle.Single -> SingleLinePrinter()
15+
OutputStyle.Verbose -> VerbosePrinter
16+
}.also {
17+
AnsiConsole.systemInstall()
18+
}
19+
20+
@VisibleForTesting
21+
internal class SingleLinePrinter : (List<ExecutionStatus.Change>) -> Unit {
22+
private var previousLineSize = 0
23+
override fun invoke(changes: List<ExecutionStatus.Change>) {
24+
val time = changes.firstOrNull()?.time ?: return
25+
changes.fold(emptyMap<String, Int>()) { acc, (_, _, current: ExecutionStatus) ->
26+
acc + Pair(
27+
first = current.state,
28+
second = acc.getOrDefault(current.state, 0) + 1
29+
)
30+
}.toList().joinToString(", ") { (status, count) ->
31+
"$status:$count"
32+
}.let { statusCounts ->
33+
print("\r" + (0..previousLineSize).joinToString("") { " " })
34+
previousLineSize = statusCounts.length
35+
print("\r${FtlConstants.indent}$time Test executions status: $statusCounts")
36+
}
37+
}
38+
}
39+
40+
@VisibleForTesting
41+
internal class MultiLinePrinter : (List<ExecutionStatus.Change>) -> Unit {
42+
private val output = LinkedHashMap<String, ExecutionStatus.View>()
43+
override fun invoke(changes: List<ExecutionStatus.Change>) {
44+
repeat(output.size) {
45+
print(Ansi.ansi().cursorUpLine().eraseLine())
46+
}
47+
output += changes.map { change ->
48+
listExecutionStatusView(change).takeLast(1)
49+
}.flatten().map { view ->
50+
view.id to view
51+
}
52+
output.values.joinToString("\n").let(::println)
53+
}
54+
}
55+
56+
@VisibleForTesting
57+
internal object VerbosePrinter : (List<ExecutionStatus.Change>) -> Unit {
58+
override fun invoke(changes: List<ExecutionStatus.Change>) {
59+
changes.map(listExecutionStatusView).flatten().forEach(::println)
60+
}
61+
}
62+
63+
private val listExecutionStatusView: (ExecutionStatus.Change) -> List<ExecutionStatus.View>
64+
get() = { (name, previous, current, time) ->
65+
emptyList<ExecutionStatus.View>()
66+
.plus(
67+
current.progress.minus(previous.progress).map { message ->
68+
ExecutionStatus.View(time, name, message)
69+
}
70+
)
71+
.let { list ->
72+
if (current.error != previous.error && current.error != null)
73+
list + ExecutionStatus.View(time, name, "Error: ${current.error}")
74+
else list
75+
}
76+
.let { list ->
77+
if (current.state != previous.state)
78+
list + ExecutionStatus.View(time, name, current.state)
79+
else list
80+
}
81+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package ftl.run.status
2+
3+
enum class OutputStyle { Verbose, Single, Multi }

0 commit comments

Comments
 (0)