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

#810 Prints matrix error as table #819

Merged
merged 10 commits into from
Jun 1, 2020
1 change: 1 addition & 0 deletions release_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
- [#805](https://github.com/Flank/flank/pull/805) Fix overlapping results. ([pawelpasterz](https://github.com/pawelpasterz))
- [#812](https://github.com/Flank/flank/issues/812) Convert bitrise macOS workflow to github action. ([piotradamczyk5](https://github.com/piotradamczyk5))
- [#799](https://github.com/Flank/flank/pull/799) Refactor Shared object by splitting it into smaller functions. ([piotradamczyk5](https://github.com/piotradamczyk5))
- [#819](https://github.com/Flank/flank/pull/819) Display matrix results in a table format. ([piotradamczyk5](https://github.com/piotradamczyk5))

## v20.05.2

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import ftl.config.FtlConstants.indent
import ftl.json.MatrixMap
import ftl.reports.util.IReport
import ftl.reports.xml.model.JUnitTestResult
import ftl.util.asPrintableTable
import ftl.util.println
import ftl.util.write
import java.io.StringWriter
Expand Down Expand Up @@ -52,6 +53,7 @@ object MatrixResultsReport : IReport {
writer.println("$indent$failed matrices failed")
writer.println()
}
writer.println(matrices.map.values.toList().asPrintableTable())

return writer.toString()
}
Expand Down
107 changes: 107 additions & 0 deletions test_runner/src/main/kotlin/ftl/util/LogTableBuilder.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package ftl.util

import com.google.common.annotations.VisibleForTesting

data class TableColumn(
val header: String,
val data: List<String>,
val columnSize: Int = header.length + DEFAULT_COLUMN_PADDING
)

private data class DataWithSize(
val data: String,
val columnSize: Int
)

private const val DEFAULT_COLUMN_PADDING = 2
@VisibleForTesting const val TABLE_HORIZONTAL_LINE = '─'
@VisibleForTesting const val TABLE_VERTICAL_LINE = '│'
@VisibleForTesting const val START_TABLE_START_CHAR = '┌'
@VisibleForTesting const val START_TABLE_MIDDLE_CHAR = '┬'
@VisibleForTesting const val START_TABLE_END_CHAR = '┐'
@VisibleForTesting const val MIDDLE_TABLE_START_CHAR = '├'
@VisibleForTesting const val MIDDLE_TABLE_MIDDLE_CHAR = '┼'
@VisibleForTesting const val MIDDLE_TABLE_END_CHAR = '┤'
@VisibleForTesting const val END_TABLE_START_CHAR = '└'
@VisibleForTesting const val END_TABLE_MIDDLE_CHAR = '┴'
@VisibleForTesting const val END_TABLE_END_CHAR = '┘'

fun buildTable(vararg tableColumns: TableColumn): String {
val rowSizes = tableColumns.map { it.columnSize }
val builder = StringBuilder().apply {
startTable(rowSizes)
tableColumns.map { DataWithSize(it.header, it.columnSize) }.apply { appendDataRow(this) }
rowSeparator(rowSizes)
appendData(tableColumns)
endTable(rowSizes)
}

return builder.toString()
}

private fun StringBuilder.startTable(rowSizes: List<Int>) {
appendTableSeparator(
startChar = START_TABLE_START_CHAR,
middleChar = START_TABLE_MIDDLE_CHAR,
endChar = START_TABLE_END_CHAR,
rowSizes = rowSizes
)
newLine()
}

private fun StringBuilder.rowSeparator(rowSizes: List<Int>) {
appendTableSeparator(
startChar = MIDDLE_TABLE_START_CHAR,
middleChar = MIDDLE_TABLE_MIDDLE_CHAR,
endChar = MIDDLE_TABLE_END_CHAR,
rowSizes = rowSizes
)
newLine()
}

private fun StringBuilder.appendData(tableColumns: Array<out TableColumn>) {
val rowCount = (tableColumns.maxBy { it.data.size } ?: tableColumns.first()).data.size

(0 until rowCount)
.map { rowNumber -> tableColumns.map { it.data.getOrNull(rowNumber).orEmpty() to it.columnSize } }
.forEach {
it.map { (data, size) -> DataWithSize(data, size) }.apply { appendDataRow(this) }
}
}

private fun StringBuilder.endTable(rowSizes: List<Int>) {
appendTableSeparator(
startChar = END_TABLE_START_CHAR,
middleChar = END_TABLE_MIDDLE_CHAR,
endChar = END_TABLE_END_CHAR,
rowSizes = rowSizes
)
}

private fun StringBuilder.appendTableSeparator(startChar: Char, middleChar: Char, endChar: Char, rowSizes: List<Int>) {
append(startChar)
rowSizes.forEachIndexed { index, rowSize ->
append(TABLE_HORIZONTAL_LINE.toString().repeat(rowSize))
append(if (rowSizes.lastIndex == index) endChar else middleChar)
}
}

private fun StringBuilder.appendDataRow(data: List<DataWithSize>) {
append(TABLE_VERTICAL_LINE)
data.forEach { (data, size) ->
append(data.center(size))
append(TABLE_VERTICAL_LINE)
}
newLine()
}

private fun String.center(columnSize: Int): String? {
return String.format(
"%-" + columnSize + "s",
String.format("%" + (length + (columnSize - length) / 2) + "s", this)
)
}

private fun StringBuilder.newLine() {
append(System.lineSeparator())
}
27 changes: 27 additions & 0 deletions test_runner/src/main/kotlin/ftl/util/SavedMatrixTableUtil.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package ftl.util

import ftl.json.SavedMatrix

fun SavedMatrix.asPrintableTable(): String {
return buildTable(
TableColumn(OUTCOME_COLUMN_HEADER, listOf(outcome), OUTCOME_COLUMN_SIZE),
TableColumn(MATRIX_ID_COLUMN_HEADER, listOf(matrixId), MATRIX_ID_COLUMN_SIZE),
TableColumn(OUTCOME_DETAILS_COLUMN_HEADER, listOf(outcomeDetails), OUTCOME_DETAILS_COLUMN_SIZE)
)
}

fun List<SavedMatrix>.asPrintableTable(): String {

return buildTable(
TableColumn(OUTCOME_COLUMN_HEADER, map { it.outcome }, OUTCOME_COLUMN_SIZE),
TableColumn(MATRIX_ID_COLUMN_HEADER, map { it.matrixId }, MATRIX_ID_COLUMN_SIZE),
TableColumn(OUTCOME_DETAILS_COLUMN_HEADER, map { it.outcomeDetails }, OUTCOME_DETAILS_COLUMN_SIZE)
)
}

private const val OUTCOME_COLUMN_HEADER = "OUTCOME"
private const val OUTCOME_COLUMN_SIZE = 9
private const val MATRIX_ID_COLUMN_HEADER = "TEST_AXIS_VALUE"
private const val MATRIX_ID_COLUMN_SIZE = 24
private const val OUTCOME_DETAILS_COLUMN_HEADER = "TEST_DETAILS"
private const val OUTCOME_DETAILS_COLUMN_SIZE = 20
9 changes: 5 additions & 4 deletions test_runner/src/main/kotlin/ftl/util/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ fun withGlobalExceptionHandling(block: () -> Int) {
exitProcess(1)
}
is FailedMatrix -> {
t.matrices.forEach { it.logError("failed") }
t.matrices.forEach { it.logError() }
if (t.ignoreFailed) exitProcess(0)
else exitProcess(1)
}
Expand All @@ -150,7 +150,7 @@ fun withGlobalExceptionHandling(block: () -> Int) {
exitProcess(1)
}
is FTLError -> {
t.matrix.logError("not finished")
t.matrix.logError()
exitProcess(3)
}
is FlankFatalError -> {
Expand All @@ -169,8 +169,9 @@ fun withGlobalExceptionHandling(block: () -> Int) {
}
}

private fun SavedMatrix.logError(message: String) {
println("Error: Matrix $message: ${this.matrixId} ${this.state} ${this.outcome} ${this.outcomeDetails} ${this.webLink}")
private fun SavedMatrix.logError() {
println("More details are available at [${this.webLink}]")
println(this.asPrintableTable())
}

fun <R : MutableMap<String, Any>, T> mutableMapProperty(
Expand Down
123 changes: 123 additions & 0 deletions test_runner/src/test/kotlin/ftl/util/LogTableBuilderTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package ftl.util

import com.google.common.truth.Truth.assertThat
import org.junit.Test

internal class LogTableBuilderTest {

private val sampleColumns = arrayOf(
TableColumn("header1", listOf("value1"), 10),
TableColumn("header2", listOf("value2"), 15),
TableColumn("header3", listOf("value3"), 20),
TableColumn("header4", listOf("value4"), 21)
)

@Test
fun `all table lines should have equal size`() {
// when
val table = buildTable(*sampleColumns)

// then
val tableLines = table.split(System.lineSeparator())
tableLines
.map { it.length }
.zipWithNext()
.forEach { (previous, next) ->
assert(previous == next)
}
}

@Test
fun `table first line should contains correct characters`() {
// given
val acceptedCharacters =
arrayOf(START_TABLE_START_CHAR, START_TABLE_MIDDLE_CHAR, START_TABLE_END_CHAR, TABLE_HORIZONTAL_LINE)

// when
val table = buildTable(*sampleColumns)

// then
val tableLines = table.split(System.lineSeparator())
val firstLine = tableLines.first()
assertThat(firstLine.first()).isEqualTo(START_TABLE_START_CHAR)
assertThat(firstLine.contains(START_TABLE_MIDDLE_CHAR)).isTrue()
assertThat(firstLine.last()).isEqualTo(START_TABLE_END_CHAR)
assertThat(firstLine.contains(TABLE_HORIZONTAL_LINE)).isTrue()
assertThat(firstLine.filterNot { acceptedCharacters.contains(it) }).isEmpty()
}

@Test
fun `table separator line should contains correct characters`() {
// given
val acceptedCharacters =
arrayOf(MIDDLE_TABLE_START_CHAR, MIDDLE_TABLE_MIDDLE_CHAR, MIDDLE_TABLE_END_CHAR, TABLE_HORIZONTAL_LINE)

// when
val table = buildTable(*sampleColumns)

// then
val tableLines = table.split(System.lineSeparator())
val firstLine = tableLines[2] // 0->table start, 1->data, 2-> row separator
assertThat(firstLine.first()).isEqualTo(MIDDLE_TABLE_START_CHAR)
assertThat(firstLine.contains(MIDDLE_TABLE_MIDDLE_CHAR)).isTrue()
assertThat(firstLine.last()).isEqualTo(MIDDLE_TABLE_END_CHAR)
assertThat(firstLine.contains(TABLE_HORIZONTAL_LINE)).isTrue()
assertThat(firstLine.filterNot { acceptedCharacters.contains(it) }).isEmpty()
}

@Test
fun `table last line should contains correct characters`() {
// given
val acceptedCharacters =
arrayOf(END_TABLE_START_CHAR, END_TABLE_MIDDLE_CHAR, END_TABLE_END_CHAR, TABLE_HORIZONTAL_LINE)

// when
val table = buildTable(*sampleColumns)

// then
val tableLines = table.split(System.lineSeparator())
val firstLine = tableLines.last()
assertThat(firstLine.first()).isEqualTo(END_TABLE_START_CHAR)
assertThat(firstLine.contains(END_TABLE_MIDDLE_CHAR)).isTrue()
assertThat(firstLine.last()).isEqualTo(END_TABLE_END_CHAR)
assertThat(firstLine.contains(TABLE_HORIZONTAL_LINE)).isTrue()
assertThat(firstLine.filterNot { acceptedCharacters.contains(it) }).isEmpty()
}

@Test
fun `table column should have correct sizes`() {
// given
val expectedColumnSizes = sampleColumns.map { it.columnSize }

// when
val table = buildTable(*sampleColumns)

// then
val tableLines = table.split(System.lineSeparator())
val headerLine = tableLines[1] // 0->table start, 1->header
val dataLine = tableLines[3] // 0->table start, 1->header, 2 -> separator, 3-> data
dataLine
.split(TABLE_VERTICAL_LINE)
.filter { it.isNotEmpty() }
.forEachIndexed { index, columnContent ->
assertThat(columnContent.length).isEqualTo(expectedColumnSizes[index])
}
headerLine
.split(TABLE_VERTICAL_LINE)
.filter { it.isNotEmpty() }
.forEachIndexed { index, columnContent ->
assertThat(columnContent.length).isEqualTo(expectedColumnSizes[index])
}
}

@Test
fun `Should not contains middle separator when having just 1 column`() {
// when
val table = buildTable(sampleColumns.first())

// then
assertThat(
table.contains(START_TABLE_MIDDLE_CHAR) || table.contains(MIDDLE_TABLE_MIDDLE_CHAR) || table.contains(END_TABLE_MIDDLE_CHAR)
).isFalse()
}
}
Loading