From dd3c53a0db67e97dcebb5a5a31156ddc2da2d8ba Mon Sep 17 00:00:00 2001 From: Jean-Michel Fayard <459464+jmfayard@users.noreply.github.com> Date: Tue, 30 Aug 2022 22:05:56 +0200 Subject: [PATCH 01/18] Add doc for writing/reading table data to a file #3179 Readme-oriented programming to make sure the proposed API make sense --- .../assertions/table_driven_testing.md | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 documentation/versioned_docs/version-5.4/assertions/table_driven_testing.md diff --git a/documentation/versioned_docs/version-5.4/assertions/table_driven_testing.md b/documentation/versioned_docs/version-5.4/assertions/table_driven_testing.md new file mode 100644 index 00000000000..a3bbf70fea5 --- /dev/null +++ b/documentation/versioned_docs/version-5.4/assertions/table_driven_testing.md @@ -0,0 +1,90 @@ +# Table-driven testing + +> TK: what current exists is not really documented anywhere, am I right? + +## Define a table in code + +You can define a table of data that will be used for your test like this: + +```kotlin +val table = table( + headers("id", "name", "username"), + row(4, "Jean-Michel Fayard", "jmfayard"), + row(6, "Louis CAD", "LouisCAD"), +) +``` + +It's now easy to run your asserts for all rows of the table: + +## Run asserts forAll rows of the table + +```kotlin +test("table-driven testing") { + table.forAll { id, name, username -> + id shouldBeGreaterThan 0 + username shouldNotBe "" + } +} +``` + +The test will not fail at the first error. Instead, it will always run on all the rows, and report multiple errors if they are present. + +Defining a table of data in code is convenient... until you start to have too much rows. + +## Export a table to a text file + +You can export your data to a text file with the `.table` extension. + +```kotlin +val tableFile = testResources.resolve("users.table") +table.writeTo(tableFile) +``` + +The `users.table` file looks like this: + +```csv +id | username | fullName +4 | jmfayard | Jean-Michel Fayard +6 | louis | Louis Caugnault +``` + +Curious why it's not just a .csv file? + +Well CSV is not a well defined format. Everyone has its flavor and we have too. The `.table` has its rules: it always uses `|` as separator, it must have an header, it can have comments, it can't have newlines inside the columns. Basically it's optimized for putting table data in a `.table` file. + +We hope you don't use Microsoft Excel to edit the CSV-like file. IntelliJ with the [CSV plugin from Martin Sommer](https://plugins.jetbrains.com/plugin/10037-csv) does that better. You can associate the `.table` extension with it and configure `|` as your CSV separator. It has a table edition mode too! + +Now that your table data lives in a file, it's time to read it! + +## Read table from files and execute your asserts + +Here is how you read your `.table` file: + +```kotlin +val tableFromFile = table( + source = testResources.resolve("users.table"), + headers = headers("id", "username", "fullName"), + transform = { a: String, b: String, c: String -> + row(a.toInt(), b, c) + } +) +``` + +The arguments are: +- the file where your table is stored +- the same headers as before: `headers("id", "username", "fullName")` +- a lambda to convert from strings (everything is a string in the text file) to the typed row you had before + + +The rest works just like before: + +```kotlin +test("table-driven testing from the .table file") { + // asserts like before + tableFromFile.forAll { id, name, username -> + id shouldBeGreaterThan 0 + username shouldNotBe "" + } +} +``` + From d47b0278157704bb88525c416e9ff518d57e520f Mon Sep 17 00:00:00 2001 From: Jean-Michel Fayard <459464+jmfayard@users.noreply.github.com> Date: Wed, 31 Aug 2022 15:15:00 +0200 Subject: [PATCH 02/18] =?UTF-8?q?=E2=9C=A8table(headers3,fileContent)=20+?= =?UTF-8?q?=20StringTable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All rows must have the right number of columns --- .../assertions/table_driven_testing.md | 3 +- .../sksamuel/kotest/data/TableParsingTest.kt | 40 ++++++++++++++++++ .../kotlin/io/kotest/data/TableParsing.kt | 41 +++++++++++++++++++ 3 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/data/TableParsingTest.kt create mode 100644 kotest-assertions/kotest-assertions-shared/src/commonMain/kotlin/io/kotest/data/TableParsing.kt diff --git a/documentation/versioned_docs/version-5.4/assertions/table_driven_testing.md b/documentation/versioned_docs/version-5.4/assertions/table_driven_testing.md index a3bbf70fea5..f10ba9fd137 100644 --- a/documentation/versioned_docs/version-5.4/assertions/table_driven_testing.md +++ b/documentation/versioned_docs/version-5.4/assertions/table_driven_testing.md @@ -62,8 +62,8 @@ Here is how you read your `.table` file: ```kotlin val tableFromFile = table( - source = testResources.resolve("users.table"), headers = headers("id", "username", "fullName"), + source = testResources.resolve("users.table"), transform = { a: String, b: String, c: String -> row(a.toInt(), b, c) } @@ -87,4 +87,3 @@ test("table-driven testing from the .table file") { } } ``` - diff --git a/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/data/TableParsingTest.kt b/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/data/TableParsingTest.kt new file mode 100644 index 00000000000..bc05f496ca3 --- /dev/null +++ b/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/data/TableParsingTest.kt @@ -0,0 +1,40 @@ +package com.sksamuel.kotest.data + +import io.kotest.assertions.throwables.shouldThrowMessage +import io.kotest.core.spec.style.FunSpec +import io.kotest.data.headers +import io.kotest.data.row +import io.kotest.data.table + +class TableParsingTest : FunSpec({ + + val headers = headers("id", "username", "fullName") + val headersText = "id | username | fullName" + val transform = { a: String, b: String, c: String -> + row(a.toInt(), b, c) + } + val expectedTable = table( + headers, + row(4, "jmfayard", "Jean-Michel Fayard"), + row(6, "louis", "Louis Caugnault"), + ) + + val validFileContent = """ + 4 | jmfayard | Jean-Michel Fayard + 6 | louis | Louis Caugnault + """.trimIndent() + + + test("All rows must have the right number of columns") { + val invalidRows = """ + 4 | jmfayard | Jean-Michel Fayard + 5 | victor | Victor Hugo | victor.hugo@guernesey.co.k + 6 | louis | Louis Caugnault + 7 | edgar + """.trimIndent() + shouldThrowMessage("Expected all rows to have size 3, but got rows at lines [1, 3]") { + table(headers, invalidRows, transform) + } + } + +}) diff --git a/kotest-assertions/kotest-assertions-shared/src/commonMain/kotlin/io/kotest/data/TableParsing.kt b/kotest-assertions/kotest-assertions-shared/src/commonMain/kotlin/io/kotest/data/TableParsing.kt new file mode 100644 index 00000000000..90172b12c03 --- /dev/null +++ b/kotest-assertions/kotest-assertions-shared/src/commonMain/kotlin/io/kotest/data/TableParsing.kt @@ -0,0 +1,41 @@ +package io.kotest.data + +import io.kotest.assertions.fail +import io.kotest.matchers.shouldBe + +// TODO I'm only supporting table with 3 arguments until the API stabilizes + +fun table( + headers: Headers3, + fileContent: String, + transform: (String, String, String) -> Row3 +): Table3 = parseTableContent(headers.values(), fileContent).let { matrix -> + val rows = matrix.map { row -> transform(row[0], row[1], row[2]) } + table(headers, *rows.toTypedArray()) +} + +internal fun parseTableContent(headers: List, fileContent: String) : List> { + val table = StringTable(headers, fileContent.lines()) + return emptyList() +} + +internal data class StringTable( + val headers: List, + val lines: List, +) { + val rows: List> = lines.map(this::parseRow) + init { + rowsShouldHaveSize(headers.size) + } + + private fun rowsShouldHaveSize(size: Int) { + val invalid = rows.withIndex() + .filter { it.value.size != size } + .map { it.index } + if (invalid.isNotEmpty()) fail("Expected all rows to have size $size, but got rows at lines $invalid") + } + + private fun parseRow(value: String): List = + value.split("|") + .map(String::trim) +} From 66c006219b61bd96aabc0f70cb0b625d212cb9b6 Mon Sep 17 00:00:00 2001 From: Jean-Michel Fayard <459464+jmfayard@users.noreply.github.com> Date: Wed, 31 Aug 2022 15:23:02 +0200 Subject: [PATCH 03/18] =?UTF-8?q?=E2=9C=A8table=20happy=20path?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sksamuel/kotest/data/TableParsingTest.kt | 4 ++++ .../kotlin/io/kotest/data/TableParsing.kt | 20 ++++++++----------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/data/TableParsingTest.kt b/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/data/TableParsingTest.kt index bc05f496ca3..46cc5f34a1a 100644 --- a/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/data/TableParsingTest.kt +++ b/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/data/TableParsingTest.kt @@ -5,6 +5,7 @@ import io.kotest.core.spec.style.FunSpec import io.kotest.data.headers import io.kotest.data.row import io.kotest.data.table +import io.kotest.matchers.shouldBe class TableParsingTest : FunSpec({ @@ -24,6 +25,9 @@ class TableParsingTest : FunSpec({ 6 | louis | Louis Caugnault """.trimIndent() + test("happy path") { + table(headers, validFileContent, transform) shouldBe expectedTable + } test("All rows must have the right number of columns") { val invalidRows = """ diff --git a/kotest-assertions/kotest-assertions-shared/src/commonMain/kotlin/io/kotest/data/TableParsing.kt b/kotest-assertions/kotest-assertions-shared/src/commonMain/kotlin/io/kotest/data/TableParsing.kt index 90172b12c03..a56e6cf5cde 100644 --- a/kotest-assertions/kotest-assertions-shared/src/commonMain/kotlin/io/kotest/data/TableParsing.kt +++ b/kotest-assertions/kotest-assertions-shared/src/commonMain/kotlin/io/kotest/data/TableParsing.kt @@ -1,7 +1,6 @@ package io.kotest.data import io.kotest.assertions.fail -import io.kotest.matchers.shouldBe // TODO I'm only supporting table with 3 arguments until the API stabilizes @@ -9,24 +8,21 @@ fun table( headers: Headers3, fileContent: String, transform: (String, String, String) -> Row3 -): Table3 = parseTableContent(headers.values(), fileContent).let { matrix -> - val rows = matrix.map { row -> transform(row[0], row[1], row[2]) } - table(headers, *rows.toTypedArray()) -} - -internal fun parseTableContent(headers: List, fileContent: String) : List> { - val table = StringTable(headers, fileContent.lines()) - return emptyList() +): Table3 { + val table = StringTable(headers.values(), fileContent.lines()) + val rows = table.mapRows { (a, b, c) -> transform(a, b, c) } + return Table3(headers, rows) } internal data class StringTable( val headers: List, val lines: List, ) { + fun mapRows(fn: (List) -> T): List = + rows.map(fn) + val rows: List> = lines.map(this::parseRow) - init { - rowsShouldHaveSize(headers.size) - } + init { rowsShouldHaveSize(headers.size) } private fun rowsShouldHaveSize(size: Int) { val invalid = rows.withIndex() From db316098af529f02fea7bd4558c83dd347dc4d71 Mon Sep 17 00:00:00 2001 From: Jean-Michel Fayard <459464+jmfayard@users.noreply.github.com> Date: Wed, 31 Aug 2022 15:53:59 +0200 Subject: [PATCH 04/18] =?UTF-8?q?=F0=9F=A5=9Atable:=20'|'=20can=20be=20esc?= =?UTF-8?q?aped=20with=20'\|'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sksamuel/kotest/data/TableParsingTest.kt | 23 +++++++++++++++++ .../kotlin/io/kotest/data/TableParsing.kt | 25 ++++++++++++++----- 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/data/TableParsingTest.kt b/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/data/TableParsingTest.kt index 46cc5f34a1a..f75c9ff0b58 100644 --- a/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/data/TableParsingTest.kt +++ b/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/data/TableParsingTest.kt @@ -29,6 +29,19 @@ class TableParsingTest : FunSpec({ table(headers, validFileContent, transform) shouldBe expectedTable } + test("empty lines and comments starting with # are accepted") { + val fileContent = """ + |4 | jmfayard | Jean-Michel Fayard + |# this is a comment + |# newlines are allowed + | + |6 | louis | Louis Caugnault + """.trimMargin() + val table = table(headers, fileContent, transform) + + table shouldBe expectedTable + } + test("All rows must have the right number of columns") { val invalidRows = """ 4 | jmfayard | Jean-Michel Fayard @@ -41,4 +54,14 @@ class TableParsingTest : FunSpec({ } } + test("The '|' character can be escaped") { + val fileContent = """ + 1 | bad \| good | name + """.trimIndent() + table(headers, fileContent, transform) shouldBe table( + headers, + row(1, "bad | good", "name") + ) + } + }) diff --git a/kotest-assertions/kotest-assertions-shared/src/commonMain/kotlin/io/kotest/data/TableParsing.kt b/kotest-assertions/kotest-assertions-shared/src/commonMain/kotlin/io/kotest/data/TableParsing.kt index a56e6cf5cde..e49fa38d9d1 100644 --- a/kotest-assertions/kotest-assertions-shared/src/commonMain/kotlin/io/kotest/data/TableParsing.kt +++ b/kotest-assertions/kotest-assertions-shared/src/commonMain/kotlin/io/kotest/data/TableParsing.kt @@ -19,19 +19,32 @@ internal data class StringTable( val lines: List, ) { fun mapRows(fn: (List) -> T): List = - rows.map(fn) + rows.map { fn(it.value) } + + val rows: List>> = + lines + .withIndex() + .filterNot { (_, line) -> + line.startsWith("#") || line.isBlank() + } + .map { it.parseRow() } - val rows: List> = lines.map(this::parseRow) init { rowsShouldHaveSize(headers.size) } private fun rowsShouldHaveSize(size: Int) { - val invalid = rows.withIndex() + val invalid = rows .filter { it.value.size != size } .map { it.index } if (invalid.isNotEmpty()) fail("Expected all rows to have size $size, but got rows at lines $invalid") } - private fun parseRow(value: String): List = - value.split("|") - .map(String::trim) + private fun IndexedValue.parseRow(): IndexedValue> { + val (index, line) = this + val notAPipeSeparator = "🫓" + return line + .replace("\\|", notAPipeSeparator) + .split("|") + .map { it.trim().replace(notAPipeSeparator, "|") } + .let { IndexedValue(index, it) } + } } From f99ed8f24b1399a11d241df872dc086f45b11f7d Mon Sep 17 00:00:00 2001 From: Jean-Michel Fayard <459464+jmfayard@users.noreply.github.com> Date: Wed, 31 Aug 2022 15:55:07 +0200 Subject: [PATCH 05/18] =?UTF-8?q?=E2=99=BB=EF=B8=8Fmove=20to=20StringTable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{TableParsingTest.kt => StringTableTest.kt} | 3 +-- .../data/{TableParsing.kt => StringTable.kt} | 12 ------------ .../src/commonMain/kotlin/io/kotest/data/tables.kt | 12 ++++++++++++ .../src/jvmMain/kotlin/io/kotest/data/TableFile.kt | 14 ++++++++++++++ 4 files changed, 27 insertions(+), 14 deletions(-) rename kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/data/{TableParsingTest.kt => StringTableTest.kt} (95%) rename kotest-assertions/kotest-assertions-shared/src/commonMain/kotlin/io/kotest/data/{TableParsing.kt => StringTable.kt} (73%) create mode 100644 kotest-assertions/kotest-assertions-shared/src/jvmMain/kotlin/io/kotest/data/TableFile.kt diff --git a/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/data/TableParsingTest.kt b/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/data/StringTableTest.kt similarity index 95% rename from kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/data/TableParsingTest.kt rename to kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/data/StringTableTest.kt index f75c9ff0b58..99e6b44ab3b 100644 --- a/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/data/TableParsingTest.kt +++ b/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/data/StringTableTest.kt @@ -7,10 +7,9 @@ import io.kotest.data.row import io.kotest.data.table import io.kotest.matchers.shouldBe -class TableParsingTest : FunSpec({ +class StringTableTest : FunSpec({ val headers = headers("id", "username", "fullName") - val headersText = "id | username | fullName" val transform = { a: String, b: String, c: String -> row(a.toInt(), b, c) } diff --git a/kotest-assertions/kotest-assertions-shared/src/commonMain/kotlin/io/kotest/data/TableParsing.kt b/kotest-assertions/kotest-assertions-shared/src/commonMain/kotlin/io/kotest/data/StringTable.kt similarity index 73% rename from kotest-assertions/kotest-assertions-shared/src/commonMain/kotlin/io/kotest/data/TableParsing.kt rename to kotest-assertions/kotest-assertions-shared/src/commonMain/kotlin/io/kotest/data/StringTable.kt index e49fa38d9d1..1819a3020a5 100644 --- a/kotest-assertions/kotest-assertions-shared/src/commonMain/kotlin/io/kotest/data/TableParsing.kt +++ b/kotest-assertions/kotest-assertions-shared/src/commonMain/kotlin/io/kotest/data/StringTable.kt @@ -2,18 +2,6 @@ package io.kotest.data import io.kotest.assertions.fail -// TODO I'm only supporting table with 3 arguments until the API stabilizes - -fun table( - headers: Headers3, - fileContent: String, - transform: (String, String, String) -> Row3 -): Table3 { - val table = StringTable(headers.values(), fileContent.lines()) - val rows = table.mapRows { (a, b, c) -> transform(a, b, c) } - return Table3(headers, rows) -} - internal data class StringTable( val headers: List, val lines: List, diff --git a/kotest-assertions/kotest-assertions-shared/src/commonMain/kotlin/io/kotest/data/tables.kt b/kotest-assertions/kotest-assertions-shared/src/commonMain/kotlin/io/kotest/data/tables.kt index fce13e882f2..bc7a479b084 100644 --- a/kotest-assertions/kotest-assertions-shared/src/commonMain/kotlin/io/kotest/data/tables.kt +++ b/kotest-assertions/kotest-assertions-shared/src/commonMain/kotlin/io/kotest/data/tables.kt @@ -45,3 +45,15 @@ data class Table19(val headers: Headers20, val rows: List>) data class Table21(val headers: Headers21, val rows: List>) data class Table22(val headers: Headers22, val rows: List>) + +// TODO I'm only supporting table with 3 arguments until the API stabilizes + +fun table( + headers: Headers3, + fileContent: String, + transform: (String, String, String) -> Row3 +): Table3 { + val table = StringTable(headers.values(), fileContent.lines()) + val rows = table.mapRows { (a, b, c) -> transform(a, b, c) } + return Table3(headers, rows) +} diff --git a/kotest-assertions/kotest-assertions-shared/src/jvmMain/kotlin/io/kotest/data/TableFile.kt b/kotest-assertions/kotest-assertions-shared/src/jvmMain/kotlin/io/kotest/data/TableFile.kt new file mode 100644 index 00000000000..d4b9141f3c7 --- /dev/null +++ b/kotest-assertions/kotest-assertions-shared/src/jvmMain/kotlin/io/kotest/data/TableFile.kt @@ -0,0 +1,14 @@ +package io.kotest.data + +// TODO I'm only supporting table with 3 arguments until the API stabilizes + +fun table( + headers: Headers3, + fileContent: String, + transform: (String, String, String) -> Row3 +): Table3 { + val table = StringTable(headers.values(), fileContent.lines()) + val rows = table.mapRows { (a, b, c) -> transform(a, b, c) } + return Table3(headers, rows) +} + From ac131a4230fa0377833e39e1d583e92107595810 Mon Sep 17 00:00:00 2001 From: Jean-Michel Fayard <459464+jmfayard@users.noreply.github.com> Date: Wed, 31 Aug 2022 16:32:22 +0200 Subject: [PATCH 06/18] =?UTF-8?q?=E2=9C=A8validating=20table=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sksamuel/kotest/data/StringTableTest.kt | 34 +++++++++++++++++++ .../resources/table/users-invalid-empty.table | 0 .../table/users-invalid-extension.csv | 3 ++ .../table/users-invalid-header.table | 3 ++ .../jvmTest/resources/table/users-valid.table | 3 ++ .../kotlin/io/kotest/data/StringTable.kt | 24 ++++++++----- .../kotlin/io/kotest/data/TableFile.kt | 17 ++++++++-- 7 files changed, 73 insertions(+), 11 deletions(-) create mode 100644 kotest-assertions/kotest-assertions-core/src/jvmTest/resources/table/users-invalid-empty.table create mode 100644 kotest-assertions/kotest-assertions-core/src/jvmTest/resources/table/users-invalid-extension.csv create mode 100644 kotest-assertions/kotest-assertions-core/src/jvmTest/resources/table/users-invalid-header.table create mode 100644 kotest-assertions/kotest-assertions-core/src/jvmTest/resources/table/users-valid.table diff --git a/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/data/StringTableTest.kt b/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/data/StringTableTest.kt index 99e6b44ab3b..7e992da4dcb 100644 --- a/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/data/StringTableTest.kt +++ b/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/data/StringTableTest.kt @@ -6,6 +6,7 @@ import io.kotest.data.headers import io.kotest.data.row import io.kotest.data.table import io.kotest.matchers.shouldBe +import java.io.File class StringTableTest : FunSpec({ @@ -63,4 +64,37 @@ class StringTableTest : FunSpec({ ) } + test("happy path for reading a table from a file") { + + } + + test("Validating table files") { + val relative = File("src/jvmTest/resources/table") + + shouldThrowMessage("Can't read table file") { + val file = relative.resolve("users-does-not-exist.table") + table(headers, file, transform) + } + + shouldThrowMessage("Table file must have a .table extension") { + val file = relative.resolve("users-invalid-extension.csv") + table(headers, file, transform) + } + + shouldThrowMessage("Table file must have a header") { + val file = relative.resolve("users-invalid-empty.table") + table(headers, file, transform) + } + + shouldThrowMessage( + """ + Missing elements from index 2 + expected:<["id", "username", "fullName"]> but was:<["id", "username"]> + """.trimIndent() + ) { + val file = relative.resolve("users-invalid-header.table") + table(headers, file, transform) + } + } + }) diff --git a/kotest-assertions/kotest-assertions-core/src/jvmTest/resources/table/users-invalid-empty.table b/kotest-assertions/kotest-assertions-core/src/jvmTest/resources/table/users-invalid-empty.table new file mode 100644 index 00000000000..e69de29bb2d diff --git a/kotest-assertions/kotest-assertions-core/src/jvmTest/resources/table/users-invalid-extension.csv b/kotest-assertions/kotest-assertions-core/src/jvmTest/resources/table/users-invalid-extension.csv new file mode 100644 index 00000000000..b50ed0bc3ef --- /dev/null +++ b/kotest-assertions/kotest-assertions-core/src/jvmTest/resources/table/users-invalid-extension.csv @@ -0,0 +1,3 @@ +id | username | fullName +4 | jmfayard | Jean-Michel Fayard +6 | louis | Louis Caugnault diff --git a/kotest-assertions/kotest-assertions-core/src/jvmTest/resources/table/users-invalid-header.table b/kotest-assertions/kotest-assertions-core/src/jvmTest/resources/table/users-invalid-header.table new file mode 100644 index 00000000000..fe7bf6629ae --- /dev/null +++ b/kotest-assertions/kotest-assertions-core/src/jvmTest/resources/table/users-invalid-header.table @@ -0,0 +1,3 @@ +id | username +4 | jmfayard +6 | louis diff --git a/kotest-assertions/kotest-assertions-core/src/jvmTest/resources/table/users-valid.table b/kotest-assertions/kotest-assertions-core/src/jvmTest/resources/table/users-valid.table new file mode 100644 index 00000000000..b50ed0bc3ef --- /dev/null +++ b/kotest-assertions/kotest-assertions-core/src/jvmTest/resources/table/users-valid.table @@ -0,0 +1,3 @@ +id | username | fullName +4 | jmfayard | Jean-Michel Fayard +6 | louis | Louis Caugnault diff --git a/kotest-assertions/kotest-assertions-shared/src/commonMain/kotlin/io/kotest/data/StringTable.kt b/kotest-assertions/kotest-assertions-shared/src/commonMain/kotlin/io/kotest/data/StringTable.kt index 1819a3020a5..f7b10330c39 100644 --- a/kotest-assertions/kotest-assertions-shared/src/commonMain/kotlin/io/kotest/data/StringTable.kt +++ b/kotest-assertions/kotest-assertions-shared/src/commonMain/kotlin/io/kotest/data/StringTable.kt @@ -5,15 +5,18 @@ import io.kotest.assertions.fail internal data class StringTable( val headers: List, val lines: List, + val skipFirstLine: Boolean = false, // for files ) { + fun mapRows(fn: (List) -> T): List = rows.map { fn(it.value) } val rows: List>> = lines .withIndex() - .filterNot { (_, line) -> - line.startsWith("#") || line.isBlank() + .filterNot { (index, line) -> + val skipHeader = index == 0 && skipFirstLine + skipHeader || line.startsWith("#") || line.isBlank() } .map { it.parseRow() } @@ -28,11 +31,16 @@ internal data class StringTable( private fun IndexedValue.parseRow(): IndexedValue> { val (index, line) = this - val notAPipeSeparator = "🫓" - return line - .replace("\\|", notAPipeSeparator) - .split("|") - .map { it.trim().replace(notAPipeSeparator, "|") } - .let { IndexedValue(index, it) } + return IndexedValue(index, Companion.parseRow(line)) + } + + companion object { + internal fun parseRow(line: String): List { + val notAPipeSeparator = "🫓" + return line + .replace("\\|", notAPipeSeparator) + .split("|") + .map { it.trim().replace(notAPipeSeparator, "|") } + } } } diff --git a/kotest-assertions/kotest-assertions-shared/src/jvmMain/kotlin/io/kotest/data/TableFile.kt b/kotest-assertions/kotest-assertions-shared/src/jvmMain/kotlin/io/kotest/data/TableFile.kt index d4b9141f3c7..9a5e1fb8f3e 100644 --- a/kotest-assertions/kotest-assertions-shared/src/jvmMain/kotlin/io/kotest/data/TableFile.kt +++ b/kotest-assertions/kotest-assertions-shared/src/jvmMain/kotlin/io/kotest/data/TableFile.kt @@ -1,14 +1,25 @@ package io.kotest.data +import io.kotest.matchers.shouldBe +import java.io.File + // TODO I'm only supporting table with 3 arguments until the API stabilizes fun table( headers: Headers3, - fileContent: String, + file: File, transform: (String, String, String) -> Row3 ): Table3 { - val table = StringTable(headers.values(), fileContent.lines()) - val rows = table.mapRows { (a, b, c) -> transform(a, b, c) } + val rows = file.toStringTable(headers.values()) + .mapRows { (a, b, c) -> transform(a, b, c) } return Table3(headers, rows) } +internal fun File.toStringTable(headers: List): StringTable { + if (exists().not()) throw AssertionError("Can't read table file") + if (extension != "table") throw AssertionError("Table file must have a .table extension") + val lines = readLines() + if (lines.isEmpty()) throw AssertionError("Table file must have a header") + StringTable.parseRow(lines.first()) shouldBe headers + return StringTable(headers, lines, skipFirstLine = true) +} From afb5e6cdf2b19b6ac204a1801fe60e22111dc11c Mon Sep 17 00:00:00 2001 From: Jean-Michel Fayard <459464+jmfayard@users.noreply.github.com> Date: Wed, 31 Aug 2022 16:34:18 +0200 Subject: [PATCH 07/18] =?UTF-8?q?=E2=9C=A8table:=20reading=20from=20a=20fi?= =?UTF-8?q?le?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/sksamuel/kotest/data/StringTableTest.kt | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/data/StringTableTest.kt b/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/data/StringTableTest.kt index 7e992da4dcb..3c141736092 100644 --- a/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/data/StringTableTest.kt +++ b/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/data/StringTableTest.kt @@ -64,25 +64,27 @@ class StringTableTest : FunSpec({ ) } - test("happy path for reading a table from a file") { + val resourcesDir = File("src/jvmTest/resources/table") + test("happy path for reading a table from a file") { + val file = resourcesDir.resolve("users-valid.table") + table(headers, file, transform) shouldBe expectedTable } test("Validating table files") { - val relative = File("src/jvmTest/resources/table") shouldThrowMessage("Can't read table file") { - val file = relative.resolve("users-does-not-exist.table") + val file = resourcesDir.resolve("users-does-not-exist.table") table(headers, file, transform) } shouldThrowMessage("Table file must have a .table extension") { - val file = relative.resolve("users-invalid-extension.csv") + val file = resourcesDir.resolve("users-invalid-extension.csv") table(headers, file, transform) } shouldThrowMessage("Table file must have a header") { - val file = relative.resolve("users-invalid-empty.table") + val file = resourcesDir.resolve("users-invalid-empty.table") table(headers, file, transform) } @@ -92,7 +94,7 @@ class StringTableTest : FunSpec({ expected:<["id", "username", "fullName"]> but was:<["id", "username"]> """.trimIndent() ) { - val file = relative.resolve("users-invalid-header.table") + val file = resourcesDir.resolve("users-invalid-header.table") table(headers, file, transform) } } From 722d3d02106f49c54f4be7625a2e8a84fa9e98c0 Mon Sep 17 00:00:00 2001 From: Jean-Michel Fayard <459464+jmfayard@users.noreply.github.com> Date: Wed, 31 Aug 2022 17:27:00 +0200 Subject: [PATCH 08/18] =?UTF-8?q?=E2=9C=A8table:=20table.writeToFile()=20h?= =?UTF-8?q?appy=20path?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sksamuel/kotest/data/StringTableTest.kt | 10 +++++++++ .../jvmTest/resources/table/writeToFile.table | 3 +++ .../kotlin/io/kotest/data/StringTable.kt | 11 ++++------ .../kotlin/io/kotest/data/TableFile.kt | 22 +++++++++++++++++-- 4 files changed, 37 insertions(+), 9 deletions(-) create mode 100644 kotest-assertions/kotest-assertions-core/src/jvmTest/resources/table/writeToFile.table diff --git a/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/data/StringTableTest.kt b/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/data/StringTableTest.kt index 3c141736092..1568681c769 100644 --- a/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/data/StringTableTest.kt +++ b/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/data/StringTableTest.kt @@ -5,6 +5,7 @@ import io.kotest.core.spec.style.FunSpec import io.kotest.data.headers import io.kotest.data.row import io.kotest.data.table +import io.kotest.data.writeToFile import io.kotest.matchers.shouldBe import java.io.File @@ -99,4 +100,13 @@ class StringTableTest : FunSpec({ } } + test("table.writeToFile() - happy path") { + val file = resourcesDir.resolve("writeToFile.table") + expectedTable.writeToFile(file) + file.readText() shouldBe """ +id | username | fullName +4 | jmfayard | Jean-Michel Fayard +6 | louis | Louis Caugnault + """.trim() + } }) diff --git a/kotest-assertions/kotest-assertions-core/src/jvmTest/resources/table/writeToFile.table b/kotest-assertions/kotest-assertions-core/src/jvmTest/resources/table/writeToFile.table new file mode 100644 index 00000000000..91f8c5bb61b --- /dev/null +++ b/kotest-assertions/kotest-assertions-core/src/jvmTest/resources/table/writeToFile.table @@ -0,0 +1,3 @@ +id | username | fullName +4 | jmfayard | Jean-Michel Fayard +6 | louis | Louis Caugnault \ No newline at end of file diff --git a/kotest-assertions/kotest-assertions-shared/src/commonMain/kotlin/io/kotest/data/StringTable.kt b/kotest-assertions/kotest-assertions-shared/src/commonMain/kotlin/io/kotest/data/StringTable.kt index f7b10330c39..2b370653d1d 100644 --- a/kotest-assertions/kotest-assertions-shared/src/commonMain/kotlin/io/kotest/data/StringTable.kt +++ b/kotest-assertions/kotest-assertions-shared/src/commonMain/kotlin/io/kotest/data/StringTable.kt @@ -18,9 +18,11 @@ internal data class StringTable( val skipHeader = index == 0 && skipFirstLine skipHeader || line.startsWith("#") || line.isBlank() } - .map { it.parseRow() } + .map { (index, line) -> IndexedValue(index, parseRow(line)) } - init { rowsShouldHaveSize(headers.size) } + init { + rowsShouldHaveSize(headers.size) + } private fun rowsShouldHaveSize(size: Int) { val invalid = rows @@ -29,11 +31,6 @@ internal data class StringTable( if (invalid.isNotEmpty()) fail("Expected all rows to have size $size, but got rows at lines $invalid") } - private fun IndexedValue.parseRow(): IndexedValue> { - val (index, line) = this - return IndexedValue(index, Companion.parseRow(line)) - } - companion object { internal fun parseRow(line: String): List { val notAPipeSeparator = "🫓" diff --git a/kotest-assertions/kotest-assertions-shared/src/jvmMain/kotlin/io/kotest/data/TableFile.kt b/kotest-assertions/kotest-assertions-shared/src/jvmMain/kotlin/io/kotest/data/TableFile.kt index 9a5e1fb8f3e..811a8ca0160 100644 --- a/kotest-assertions/kotest-assertions-shared/src/jvmMain/kotlin/io/kotest/data/TableFile.kt +++ b/kotest-assertions/kotest-assertions-shared/src/jvmMain/kotlin/io/kotest/data/TableFile.kt @@ -10,12 +10,17 @@ fun table( file: File, transform: (String, String, String) -> Row3 ): Table3 { - val rows = file.toStringTable(headers.values()) + val rows = file.readStringTable(headers.values()) .mapRows { (a, b, c) -> transform(a, b, c) } return Table3(headers, rows) } -internal fun File.toStringTable(headers: List): StringTable { +fun Table3.writeToFile(file: File) { + val cells = rows.map { row -> row.values().map { it.toString() } } + writeToFile(file, headers.values(), cells) +} + +internal fun File.readStringTable(headers: List): StringTable { if (exists().not()) throw AssertionError("Can't read table file") if (extension != "table") throw AssertionError("Table file must have a .table extension") val lines = readLines() @@ -23,3 +28,16 @@ internal fun File.toStringTable(headers: List): StringTable { StringTable.parseRow(lines.first()) shouldBe headers return StringTable(headers, lines, skipFirstLine = true) } + +fun writeToFile(file: File, headers: List, cells: List>) { + val separator = " | " + val formattedHeader = headers.joinToString(separator) + val formattedContent = cells.joinToString("\n") { row -> + row.joinToString(separator) + } + + file.writeText(""" +$formattedHeader +$formattedContent + """.trimIndent()) +} From 9bb9e315665c09d0a93d5fbf1353227b9a7fe819 Mon Sep 17 00:00:00 2001 From: Jean-Michel Fayard <459464+jmfayard@users.noreply.github.com> Date: Wed, 31 Aug 2022 22:32:49 +0200 Subject: [PATCH 09/18] =?UTF-8?q?=E2=9C=A8map.toTable()?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/sksamuel/kotest/data/StringTableTest.kt | 13 +++++++++++++ .../src/commonMain/kotlin/io/kotest/data/tables.kt | 5 +++++ 2 files changed, 18 insertions(+) diff --git a/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/data/StringTableTest.kt b/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/data/StringTableTest.kt index 1568681c769..dda3237e205 100644 --- a/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/data/StringTableTest.kt +++ b/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/data/StringTableTest.kt @@ -5,6 +5,7 @@ import io.kotest.core.spec.style.FunSpec import io.kotest.data.headers import io.kotest.data.row import io.kotest.data.table +import io.kotest.data.toTable import io.kotest.data.writeToFile import io.kotest.matchers.shouldBe import java.io.File @@ -26,6 +27,18 @@ class StringTableTest : FunSpec({ 6 | louis | Louis Caugnault """.trimIndent() + test("create table from map") { + val map = mapOf( + "fr" to "French", + "es" to "Spanish" + ) + val table = map.toTable(headers("code", "language")) + table shouldBe table( + headers("code", "language"), + row("fr", "French"), + row("es", "Spanish"), + ) + test("happy path") { table(headers, validFileContent, transform) shouldBe expectedTable } diff --git a/kotest-assertions/kotest-assertions-shared/src/commonMain/kotlin/io/kotest/data/tables.kt b/kotest-assertions/kotest-assertions-shared/src/commonMain/kotlin/io/kotest/data/tables.kt index bc7a479b084..86c1e9ba256 100644 --- a/kotest-assertions/kotest-assertions-shared/src/commonMain/kotlin/io/kotest/data/tables.kt +++ b/kotest-assertions/kotest-assertions-shared/src/commonMain/kotlin/io/kotest/data/tables.kt @@ -1,5 +1,10 @@ package io.kotest.data +fun Map.toTable( + headers: Headers2 = headers("key", "value"), +) = Table2(headers, entries.map { row(it.key, it.value) }) + + fun table(headers: Headers1, vararg rows: Row1) = Table1(headers, rows.asList()) fun table(headers: Headers2, vararg rows: Row2) = Table2(headers, rows.asList()) fun table(headers: Headers3, vararg rows: Row3) = Table3(headers, rows.asList()) From d376d4f8bb6b8e5e9b006c35ab15a3718a12040a Mon Sep 17 00:00:00 2001 From: Jean-Michel Fayard <459464+jmfayard@users.noreply.github.com> Date: Wed, 31 Aug 2022 22:43:33 +0200 Subject: [PATCH 10/18] =?UTF-8?q?=E2=9C=A8table(headers(...),=20list.map?= =?UTF-8?q?=20{=20row(...)=20})?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sksamuel/kotest/data/StringTableTest.kt | 43 ++++++++++++++----- .../kotlin/io/kotest/data/tables.kt | 3 ++ 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/data/StringTableTest.kt b/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/data/StringTableTest.kt index dda3237e205..def4bccc55a 100644 --- a/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/data/StringTableTest.kt +++ b/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/data/StringTableTest.kt @@ -27,17 +27,38 @@ class StringTableTest : FunSpec({ 6 | louis | Louis Caugnault """.trimIndent() - test("create table from map") { - val map = mapOf( - "fr" to "French", - "es" to "Spanish" - ) - val table = map.toTable(headers("code", "language")) - table shouldBe table( - headers("code", "language"), - row("fr", "French"), - row("es", "Spanish"), - ) + context("creating tables from collections") { + test("create table from map") { + val map = mapOf( + "fr" to "French", + "es" to "Spanish" + ) + val table = map.toTable(headers("code", "language")) + table shouldBe table( + headers("code", "language"), + row("fr", "French"), + row("es", "Spanish"), + ) + } + + test("create table from list") { + data class Language(val code: String, val english: String, val name: String) + + val languages = listOf( + Language("fr", "French", "Français"), + Language("es", "Spanish", "Español"), + ) + val table = table( + headers("code", "name", "english"), + languages.map { row(it.code, it.name, it.english) } + ) + table shouldBe table( + headers("code", "name", "english"), + row("fr", "Français", "French"), + row("es", "Español", "Spanish"), + ) + } + } test("happy path") { table(headers, validFileContent, transform) shouldBe expectedTable diff --git a/kotest-assertions/kotest-assertions-shared/src/commonMain/kotlin/io/kotest/data/tables.kt b/kotest-assertions/kotest-assertions-shared/src/commonMain/kotlin/io/kotest/data/tables.kt index 86c1e9ba256..bf1a9ea44e8 100644 --- a/kotest-assertions/kotest-assertions-shared/src/commonMain/kotlin/io/kotest/data/tables.kt +++ b/kotest-assertions/kotest-assertions-shared/src/commonMain/kotlin/io/kotest/data/tables.kt @@ -4,6 +4,9 @@ fun Map.toTable( headers: Headers2 = headers("key", "value"), ) = Table2(headers, entries.map { row(it.key, it.value) }) +// only 3 arguments for now +fun table(headers: Headers3, rows: List>) = Table3(headers, rows) + fun table(headers: Headers1, vararg rows: Row1) = Table1(headers, rows.asList()) fun table(headers: Headers2, vararg rows: Row2) = Table2(headers, rows.asList()) From c6bc19197d111988b1e6a2bac072d51d6f7ee207 Mon Sep 17 00:00:00 2001 From: Jean-Michel Fayard <459464+jmfayard@users.noreply.github.com> Date: Wed, 31 Aug 2022 23:27:23 +0200 Subject: [PATCH 11/18] =?UTF-8?q?=E2=9C=A8file.writeTable(headers,=20rows)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sksamuel/kotest/data/StringTableTest.kt | 57 ++++++++---- .../jvmTest/resources/table/writeToFile.table | 3 - .../commonMain/kotlin/io/kotest/data/rows.kt | 92 ++++++++++--------- .../kotlin/io/kotest/data/tables.kt | 10 +- .../kotlin/io/kotest/data/TableFile.kt | 24 +++-- 5 files changed, 113 insertions(+), 73 deletions(-) delete mode 100644 kotest-assertions/kotest-assertions-core/src/jvmTest/resources/table/writeToFile.table diff --git a/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/data/StringTableTest.kt b/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/data/StringTableTest.kt index def4bccc55a..935c581d9f9 100644 --- a/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/data/StringTableTest.kt +++ b/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/data/StringTableTest.kt @@ -3,10 +3,12 @@ package com.sksamuel.kotest.data import io.kotest.assertions.throwables.shouldThrowMessage import io.kotest.core.spec.style.FunSpec import io.kotest.data.headers +import io.kotest.data.mapRows import io.kotest.data.row import io.kotest.data.table import io.kotest.data.toTable -import io.kotest.data.writeToFile +import io.kotest.data.writeTable +import io.kotest.engine.spec.tempfile import io.kotest.matchers.shouldBe import java.io.File @@ -41,22 +43,25 @@ class StringTableTest : FunSpec({ ) } - test("create table from list") { - data class Language(val code: String, val english: String, val name: String) + val languagesTable = table( + headers("code", "name", "english"), + row("fr", "Français", "French"), + row("es", "Español", "Spanish"), + ) - val languages = listOf( - Language("fr", "French", "Français"), - Language("es", "Spanish", "Español"), - ) + data class Language(val code: String, val english: String, val name: String) + + val languages = listOf( + Language("fr", "French", "Français"), + Language("es", "Spanish", "Español"), + ) + + test("create table from list") { val table = table( headers("code", "name", "english"), languages.map { row(it.code, it.name, it.english) } ) - table shouldBe table( - headers("code", "name", "english"), - row("fr", "Français", "French"), - row("es", "Español", "Spanish"), - ) + table shouldBe languagesTable } } @@ -134,13 +139,31 @@ class StringTableTest : FunSpec({ } } - test("table.writeToFile() - happy path") { - val file = resourcesDir.resolve("writeToFile.table") - expectedTable.writeToFile(file) - file.readText() shouldBe """ + context("file.writeTable(headers, rows)") { + data class UserInfo(val username: String, val fullName: String) + + val expectedFileContent = """ id | username | fullName 4 | jmfayard | Jean-Michel Fayard 6 | louis | Louis Caugnault """.trim() + + val table = table( + headers("id", "UserInfo"), + row(4, UserInfo("jmfayard", "Jean-Michel Fayard")), + row(6, UserInfo("louis", "Louis Caugnault")) + ) + + test("happy path") { + val file = tempfile() + val rows = table.mapRows { (id, userInfo) -> + row(id.toString(), userInfo.username, userInfo.fullName) + } + val fileContent = file.writeTable(headers("id", "username", "fullName"), rows) + file.readText() shouldBe expectedFileContent + fileContent shouldBe expectedFileContent + } } -}) + +} +) diff --git a/kotest-assertions/kotest-assertions-core/src/jvmTest/resources/table/writeToFile.table b/kotest-assertions/kotest-assertions-core/src/jvmTest/resources/table/writeToFile.table deleted file mode 100644 index 91f8c5bb61b..00000000000 --- a/kotest-assertions/kotest-assertions-core/src/jvmTest/resources/table/writeToFile.table +++ /dev/null @@ -1,3 +0,0 @@ -id | username | fullName -4 | jmfayard | Jean-Michel Fayard -6 | louis | Louis Caugnault \ No newline at end of file diff --git a/kotest-assertions/kotest-assertions-shared/src/commonMain/kotlin/io/kotest/data/rows.kt b/kotest-assertions/kotest-assertions-shared/src/commonMain/kotlin/io/kotest/data/rows.kt index 7ac33b0f6be..2f76b39a81c 100644 --- a/kotest-assertions/kotest-assertions-shared/src/commonMain/kotlin/io/kotest/data/rows.kt +++ b/kotest-assertions/kotest-assertions-shared/src/commonMain/kotlin/io/kotest/data/rows.kt @@ -23,90 +23,94 @@ fun row(a: A, b: B, fun row(a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: S, t: T, u: U) = Row21(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u) fun row(a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L, m: M, n: N, o: O, p: P, q: Q, r: R, s: S, t: T, u: U, v: V) = Row22(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v) -data class Row1(val a: A) { - fun values() = listOf(a) +interface Row { + fun values(): List } -data class Row2(val a: A, val b: B) { - fun values() = listOf(a, b) +data class Row1(val a: A): Row { + override fun values() = listOf(a) } -data class Row3(val a: A, val b: B, val c: C) { - fun values() = listOf(a, b, c) +data class Row2(val a: A, val b: B): Row { + override fun values() = listOf(a, b) } -data class Row4(val a: A, val b: B, val c: C, val d: D) { - fun values() = listOf(a, b, c, d) +data class Row3(val a: A, val b: B, val c: C): Row { + override fun values() = listOf(a, b, c) } -data class Row5(val a: A, val b: B, val c: C, val d: D, val e: E) { - fun values() = listOf(a, b, c, d, e) +data class Row4(val a: A, val b: B, val c: C, val d: D): Row { + override fun values(): List = listOf(a, b, c, d) } -data class Row6(val a: A, val b: B, val c: C, val d: D, val e: E, val f: F) { - fun values() = listOf(a, b, c, d, e, f) +data class Row5(val a: A, val b: B, val c: C, val d: D, val e: E): Row { + override fun values() = listOf(a, b, c, d, e) } -data class Row7(val a: A, val b: B, val c: C, val d: D, val e: E, val f: F, val g: G) { - fun values() = listOf(a, b, c, d, e, f, g) +data class Row6(val a: A, val b: B, val c: C, val d: D, val e: E, val f: F): Row { + override fun values() = listOf(a, b, c, d, e, f) } -data class Row8(val a: A, val b: B, val c: C, val d: D, val e: E, val f: F, val g: G, val h: H) { - fun values() = listOf(a, b, c, d, e, f, g, h) +data class Row7(val a: A, val b: B, val c: C, val d: D, val e: E, val f: F, val g: G): Row { + override fun values() = listOf(a, b, c, d, e, f, g) } -data class Row9(val a: A, val b: B, val c: C, val d: D, val e: E, val f: F, val g: G, val h: H, val i: I) { - fun values() = listOf(a, b, c, d, e, f, g, h, i) +data class Row8(val a: A, val b: B, val c: C, val d: D, val e: E, val f: F, val g: G, val h: H): Row { + override fun values() = listOf(a, b, c, d, e, f, g, h) } -data class Row10(val a: A, val b: B, val c: C, val d: D, val e: E, val f: F, val g: G, val h: H, val i: I, val j: J) { - fun values() = listOf(a, b, c, d, e, f, g, h, i, j) +data class Row9(val a: A, val b: B, val c: C, val d: D, val e: E, val f: F, val g: G, val h: H, val i: I): Row { + override fun values() = listOf(a, b, c, d, e, f, g, h, i) } -data class Row11(val a: A, val b: B, val c: C, val d: D, val e: E, val f: F, val g: G, val h: H, val i: I, val j: J, val k: K) { - fun values() = listOf(a, b, c, d, e, f, g, h, i, j, k) +data class Row10(val a: A, val b: B, val c: C, val d: D, val e: E, val f: F, val g: G, val h: H, val i: I, val j: J): Row { + override fun values() = listOf(a, b, c, d, e, f, g, h, i, j) } -data class Row12(val a: A, val b: B, val c: C, val d: D, val e: E, val f: F, val g: G, val h: H, val i: I, val j: J, val k: K, val l: L) { - fun values() = listOf(a, b, c, d, e, f, g, h, i, j, k, l) +data class Row11(val a: A, val b: B, val c: C, val d: D, val e: E, val f: F, val g: G, val h: H, val i: I, val j: J, val k: K): Row { + override fun values() = listOf(a, b, c, d, e, f, g, h, i, j, k) } -data class Row13(val a: A, val b: B, val c: C, val d: D, val e: E, val f: F, val g: G, val h: H, val i: I, val j: J, val k: K, val l: L, val m: M) { - fun values() = listOf(a, b, c, d, e, f, g, h, i, j, k, l, m) +data class Row12(val a: A, val b: B, val c: C, val d: D, val e: E, val f: F, val g: G, val h: H, val i: I, val j: J, val k: K, val l: L): Row { + override fun values() = listOf(a, b, c, d, e, f, g, h, i, j, k, l) } -data class Row14(val a: A, val b: B, val c: C, val d: D, val e: E, val f: F, val g: G, val h: H, val i: I, val j: J, val k: K, val l: L, val m: M, val n: N) { - fun values() = listOf(a, b, c, d, e, f, g, h, i, j, k, l, m, n) +data class Row13(val a: A, val b: B, val c: C, val d: D, val e: E, val f: F, val g: G, val h: H, val i: I, val j: J, val k: K, val l: L, val m: M): Row { + override fun values() = listOf(a, b, c, d, e, f, g, h, i, j, k, l, m) } -data class Row15(val a: A, val b: B, val c: C, val d: D, val e: E, val f: F, val g: G, val h: H, val i: I, val j: J, val k: K, val l: L, val m: M, val n: N, val o: O) { - fun values() = listOf(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o) +data class Row14(val a: A, val b: B, val c: C, val d: D, val e: E, val f: F, val g: G, val h: H, val i: I, val j: J, val k: K, val l: L, val m: M, val n: N): Row { + override fun values() = listOf(a, b, c, d, e, f, g, h, i, j, k, l, m, n) } -data class Row16(val a: A, val b: B, val c: C, val d: D, val e: E, val f: F, val g: G, val h: H, val i: I, val j: J, val k: K, val l: L, val m: M, val n: N, val o: O, val p: P) { - fun values() = listOf(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p) +data class Row15(val a: A, val b: B, val c: C, val d: D, val e: E, val f: F, val g: G, val h: H, val i: I, val j: J, val k: K, val l: L, val m: M, val n: N, val o: O): Row { + override fun values() = listOf(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o) } -data class Row17(val a: A, val b: B, val c: C, val d: D, val e: E, val f: F, val g: G, val h: H, val i: I, val j: J, val k: K, val l: L, val m: M, val n: N, val o: O, val p: P, val q: Q) { - fun values() = listOf(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q) +data class Row16(val a: A, val b: B, val c: C, val d: D, val e: E, val f: F, val g: G, val h: H, val i: I, val j: J, val k: K, val l: L, val m: M, val n: N, val o: O, val p: P): Row { + override fun values() = listOf(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p) } -data class Row18(val a: A, val b: B, val c: C, val d: D, val e: E, val f: F, val g: G, val h: H, val i: I, val j: J, val k: K, val l: L, val m: M, val n: N, val o: O, val p: P, val q: Q, val r: R) { - fun values() = listOf(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r) +data class Row17(val a: A, val b: B, val c: C, val d: D, val e: E, val f: F, val g: G, val h: H, val i: I, val j: J, val k: K, val l: L, val m: M, val n: N, val o: O, val p: P, val q: Q): Row { + override fun values() = listOf(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q) } -data class Row19(val a: A, val b: B, val c: C, val d: D, val e: E, val f: F, val g: G, val h: H, val i: I, val j: J, val k: K, val l: L, val m: M, val n: N, val o: O, val p: P, val q: Q, val r: R, val s: S) { - fun values() = listOf(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s) +data class Row18(val a: A, val b: B, val c: C, val d: D, val e: E, val f: F, val g: G, val h: H, val i: I, val j: J, val k: K, val l: L, val m: M, val n: N, val o: O, val p: P, val q: Q, val r: R): Row { + override fun values() = listOf(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r) } -data class Row20(val a: A, val b: B, val c: C, val d: D, val e: E, val f: F, val g: G, val h: H, val i: I, val j: J, val k: K, val l: L, val m: M, val n: N, val o: O, val p: P, val q: Q, val r: R, val s: S, val t: T) { - fun values() = listOf(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t) +data class Row19(val a: A, val b: B, val c: C, val d: D, val e: E, val f: F, val g: G, val h: H, val i: I, val j: J, val k: K, val l: L, val m: M, val n: N, val o: O, val p: P, val q: Q, val r: R, val s: S): Row { + override fun values() = listOf(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s) } -data class Row21(val a: A, val b: B, val c: C, val d: D, val e: E, val f: F, val g: G, val h: H, val i: I, val j: J, val k: K, val l: L, val m: M, val n: N, val o: O, val p: P, val q: Q, val r: R, val s: S, val t: T, val u: U) { - fun values() = listOf(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u) +data class Row20(val a: A, val b: B, val c: C, val d: D, val e: E, val f: F, val g: G, val h: H, val i: I, val j: J, val k: K, val l: L, val m: M, val n: N, val o: O, val p: P, val q: Q, val r: R, val s: S, val t: T): Row { + override fun values() = listOf(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t) } -data class Row22(val a: A, val b: B, val c: C, val d: D, val e: E, val f: F, val g: G, val h: H, val i: I, val j: J, val k: K, val l: L, val m: M, val n: N, val o: O, val p: P, val q: Q, val r: R, val s: S, val t: T, val u: U, val v: V) { - fun values() = listOf(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v) +data class Row21(val a: A, val b: B, val c: C, val d: D, val e: E, val f: F, val g: G, val h: H, val i: I, val j: J, val k: K, val l: L, val m: M, val n: N, val o: O, val p: P, val q: Q, val r: R, val s: S, val t: T, val u: U): Row { + override fun values() = listOf(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u) +} + +data class Row22(val a: A, val b: B, val c: C, val d: D, val e: E, val f: F, val g: G, val h: H, val i: I, val j: J, val k: K, val l: L, val m: M, val n: N, val o: O, val p: P, val q: Q, val r: R, val s: S, val t: T, val u: U, val v: V): Row { + override fun values() = listOf(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v) } diff --git a/kotest-assertions/kotest-assertions-shared/src/commonMain/kotlin/io/kotest/data/tables.kt b/kotest-assertions/kotest-assertions-shared/src/commonMain/kotlin/io/kotest/data/tables.kt index bf1a9ea44e8..4ec4ade013a 100644 --- a/kotest-assertions/kotest-assertions-shared/src/commonMain/kotlin/io/kotest/data/tables.kt +++ b/kotest-assertions/kotest-assertions-shared/src/commonMain/kotlin/io/kotest/data/tables.kt @@ -2,11 +2,17 @@ package io.kotest.data fun Map.toTable( headers: Headers2 = headers("key", "value"), -) = Table2(headers, entries.map { row(it.key, it.value) }) +) = table(headers, entries.map { row(it.key, it.value) }) -// only 3 arguments for now +fun table(headers: Headers1, rows: List>) = Table1(headers, rows) +fun table(headers: Headers2, rows: List>) = Table2(headers, rows) fun table(headers: Headers3, rows: List>) = Table3(headers, rows) +// TODO more +fun Table1.mapRows(fn: (Row1) -> T): List = rows.map(fn) +fun Table2.mapRows(fn: (Row2) -> T): List = rows.map(fn) +fun Table3.mapRows(fn: (Row3) -> T): List = rows.map(fn) +// TODO more fun table(headers: Headers1, vararg rows: Row1) = Table1(headers, rows.asList()) fun table(headers: Headers2, vararg rows: Row2) = Table2(headers, rows.asList()) diff --git a/kotest-assertions/kotest-assertions-shared/src/jvmMain/kotlin/io/kotest/data/TableFile.kt b/kotest-assertions/kotest-assertions-shared/src/jvmMain/kotlin/io/kotest/data/TableFile.kt index 811a8ca0160..867e605c661 100644 --- a/kotest-assertions/kotest-assertions-shared/src/jvmMain/kotlin/io/kotest/data/TableFile.kt +++ b/kotest-assertions/kotest-assertions-shared/src/jvmMain/kotlin/io/kotest/data/TableFile.kt @@ -15,10 +15,16 @@ fun table( return Table3(headers, rows) } -fun Table3.writeToFile(file: File) { - val cells = rows.map { row -> row.values().map { it.toString() } } - writeToFile(file, headers.values(), cells) -} +fun File.writeTable(headers: Headers1, rows: List>): String = + writeTable(headers.values(), rows.map { it.strings() } ) +fun File.writeTable(headers: Headers2, rows: List>): String = + writeTable(headers.values(), rows.map { it.strings() } ) +fun File.writeTable(headers: Headers3, rows: List>): String = + writeTable(headers.values(), rows.map { it.strings() } ) +// TODO + +private fun Row.strings(): List = values().map { it.toString() } + internal fun File.readStringTable(headers: List): StringTable { if (exists().not()) throw AssertionError("Can't read table file") @@ -29,15 +35,19 @@ internal fun File.readStringTable(headers: List): StringTable { return StringTable(headers, lines, skipFirstLine = true) } -fun writeToFile(file: File, headers: List, cells: List>) { +fun File.writeTable(headers: List, cells: List>): String { + if (extension != "table") throw AssertionError("Table file must have a .table extension") + val separator = " | " val formattedHeader = headers.joinToString(separator) val formattedContent = cells.joinToString("\n") { row -> row.joinToString(separator) } - file.writeText(""" + val fileContent = """ $formattedHeader $formattedContent - """.trimIndent()) + """.trimIndent() + writeText(fileContent) + return fileContent } From 211817e7060b0f4f9bd8721a06c4bd75cd53f6bf Mon Sep 17 00:00:00 2001 From: Jean-Michel Fayard <459464+jmfayard@users.noreply.github.com> Date: Wed, 31 Aug 2022 23:40:35 +0200 Subject: [PATCH 12/18] =?UTF-8?q?=E2=9C=85file.write(headers,=20rows):=20v?= =?UTF-8?q?alidation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sksamuel/kotest/data/StringTableTest.kt | 35 +++++++++++++------ .../kotlin/io/kotest/data/TableFile.kt | 3 +- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/data/StringTableTest.kt b/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/data/StringTableTest.kt index 935c581d9f9..ef76daf2d6c 100644 --- a/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/data/StringTableTest.kt +++ b/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/data/StringTableTest.kt @@ -139,23 +139,22 @@ class StringTableTest : FunSpec({ } } - context("file.writeTable(headers, rows)") { - data class UserInfo(val username: String, val fullName: String) + data class UserInfo(val username: String, val fullName: String) + val table = table( + headers("id", "UserInfo"), + row(4, UserInfo("jmfayard", "Jean-Michel Fayard")), + row(6, UserInfo("louis", "Louis Caugnault")) + ) + context("file.writeTable - success") { val expectedFileContent = """ id | username | fullName 4 | jmfayard | Jean-Michel Fayard 6 | louis | Louis Caugnault """.trim() - val table = table( - headers("id", "UserInfo"), - row(4, UserInfo("jmfayard", "Jean-Michel Fayard")), - row(6, UserInfo("louis", "Louis Caugnault")) - ) - test("happy path") { - val file = tempfile() + val file = tempfile(suffix = ".table") val rows = table.mapRows { (id, userInfo) -> row(id.toString(), userInfo.username, userInfo.fullName) } @@ -165,5 +164,19 @@ id | username | fullName } } -} -) + context("file.writeTable - validation") { + test("Table file must have a .table extension") { + shouldThrowMessage(testCase.name.testName) { + tempfile().writeTable(table.headers, emptyList()) + } + } + + test("Cells con't contain new lines") { + val table = + mapOf("1" to "one\n", "two" to "two", "three" to "three\nthree").toTable() + shouldThrowMessage(testCase.name.testName) { + tempfile(suffix = ".table").writeTable(table.headers, table.rows) + } + } + } +}) diff --git a/kotest-assertions/kotest-assertions-shared/src/jvmMain/kotlin/io/kotest/data/TableFile.kt b/kotest-assertions/kotest-assertions-shared/src/jvmMain/kotlin/io/kotest/data/TableFile.kt index 867e605c661..86bd99e989b 100644 --- a/kotest-assertions/kotest-assertions-shared/src/jvmMain/kotlin/io/kotest/data/TableFile.kt +++ b/kotest-assertions/kotest-assertions-shared/src/jvmMain/kotlin/io/kotest/data/TableFile.kt @@ -37,7 +37,8 @@ internal fun File.readStringTable(headers: List): StringTable { fun File.writeTable(headers: List, cells: List>): String { if (extension != "table") throw AssertionError("Table file must have a .table extension") - + val containsNewLines = cells.any { it.any { cell -> cell.contains("\n") } } + if (containsNewLines) throw AssertionError("Cells con't contain new lines") val separator = " | " val formattedHeader = headers.joinToString(separator) val formattedContent = cells.joinToString("\n") { row -> From 88fab6ba23eca62f37b0da504f7a4b7e1aa6d7cd Mon Sep 17 00:00:00 2001 From: Jean-Michel Fayard <459464+jmfayard@users.noreply.github.com> Date: Wed, 31 Aug 2022 23:46:46 +0200 Subject: [PATCH 13/18] =?UTF-8?q?=E2=9C=85escape=20pipe?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sksamuel/kotest/data/StringTableTest.kt | 20 ++++++++++++++----- .../kotlin/io/kotest/data/TableFile.kt | 2 +- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/data/StringTableTest.kt b/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/data/StringTableTest.kt index ef76daf2d6c..181cd4c1001 100644 --- a/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/data/StringTableTest.kt +++ b/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/data/StringTableTest.kt @@ -140,7 +140,7 @@ class StringTableTest : FunSpec({ } data class UserInfo(val username: String, val fullName: String) - val table = table( + val usersTable = table( headers("id", "UserInfo"), row(4, UserInfo("jmfayard", "Jean-Michel Fayard")), row(6, UserInfo("louis", "Louis Caugnault")) @@ -155,27 +155,37 @@ id | username | fullName test("happy path") { val file = tempfile(suffix = ".table") - val rows = table.mapRows { (id, userInfo) -> + val rows = usersTable.mapRows { (id, userInfo) -> row(id.toString(), userInfo.username, userInfo.fullName) } val fileContent = file.writeTable(headers("id", "username", "fullName"), rows) file.readText() shouldBe expectedFileContent fileContent shouldBe expectedFileContent } + + test("| should be escaped") { + val table = mapOf("greeting" to "Hello || world").toTable() + val file = tempfile(suffix = ".table") + file.writeTable(table.headers, table.rows) shouldBe """ + key | value + greeting | Hello \|\| world + """.trimIndent() + } } context("file.writeTable - validation") { test("Table file must have a .table extension") { shouldThrowMessage(testCase.name.testName) { - tempfile().writeTable(table.headers, emptyList()) + val fileMissingTableExtension = tempfile() + fileMissingTableExtension.writeTable(usersTable.headers, emptyList()) } } test("Cells con't contain new lines") { - val table = + val tableWithNewLines = mapOf("1" to "one\n", "two" to "two", "three" to "three\nthree").toTable() shouldThrowMessage(testCase.name.testName) { - tempfile(suffix = ".table").writeTable(table.headers, table.rows) + tempfile(suffix = ".table").writeTable(tableWithNewLines.headers, tableWithNewLines.rows) } } } diff --git a/kotest-assertions/kotest-assertions-shared/src/jvmMain/kotlin/io/kotest/data/TableFile.kt b/kotest-assertions/kotest-assertions-shared/src/jvmMain/kotlin/io/kotest/data/TableFile.kt index 86bd99e989b..9d5860195f0 100644 --- a/kotest-assertions/kotest-assertions-shared/src/jvmMain/kotlin/io/kotest/data/TableFile.kt +++ b/kotest-assertions/kotest-assertions-shared/src/jvmMain/kotlin/io/kotest/data/TableFile.kt @@ -42,7 +42,7 @@ fun File.writeTable(headers: List, cells: List>): String { val separator = " | " val formattedHeader = headers.joinToString(separator) val formattedContent = cells.joinToString("\n") { row -> - row.joinToString(separator) + row.joinToString(separator) { it.replace("|", "\\|") } } val fileContent = """ From 1957d2f4ea94594504720ead5e4a0aec3d97f008 Mon Sep 17 00:00:00 2001 From: Jean-Michel Fayard <459464+jmfayard@users.noreply.github.com> Date: Thu, 1 Sep 2022 00:11:51 +0200 Subject: [PATCH 14/18] =?UTF-8?q?=F0=9F=8E=89=20tables=20are=20now=20align?= =?UTF-8?q?ed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sksamuel/kotest/data/StringTableTest.kt | 27 ++++++++++++++++--- .../kotlin/io/kotest/data/TableFile.kt | 21 +++++++++++++-- 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/data/StringTableTest.kt b/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/data/StringTableTest.kt index 181cd4c1001..ff308b5de0d 100644 --- a/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/data/StringTableTest.kt +++ b/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/data/StringTableTest.kt @@ -2,6 +2,7 @@ package com.sksamuel.kotest.data import io.kotest.assertions.throwables.shouldThrowMessage import io.kotest.core.spec.style.FunSpec +import io.kotest.data.Row3 import io.kotest.data.headers import io.kotest.data.mapRows import io.kotest.data.row @@ -149,8 +150,8 @@ class StringTableTest : FunSpec({ context("file.writeTable - success") { val expectedFileContent = """ id | username | fullName -4 | jmfayard | Jean-Michel Fayard -6 | louis | Louis Caugnault +4 | jmfayard | Jean-Michel Fayard +6 | louis | Louis Caugnault """.trim() test("happy path") { @@ -163,11 +164,31 @@ id | username | fullName fileContent shouldBe expectedFileContent } + test("columns should be aligned") { + fun row(i: Int): Row3 { + val value = "$i".repeat(i) + return row(value, value, value) + } + + val table = table( + headers("a", "b", "c"), + row(2), + row(4), + row(6), + ) + tempfile(suffix = ".table").writeTable(table.headers, table.rows) shouldBe """ +a | b | c +22 | 22 | 22 +4444 | 4444 | 4444 +666666 | 666666 | 666666 + """.trimIndent() + } + test("| should be escaped") { val table = mapOf("greeting" to "Hello || world").toTable() val file = tempfile(suffix = ".table") file.writeTable(table.headers, table.rows) shouldBe """ - key | value + key | value greeting | Hello \|\| world """.trimIndent() } diff --git a/kotest-assertions/kotest-assertions-shared/src/jvmMain/kotlin/io/kotest/data/TableFile.kt b/kotest-assertions/kotest-assertions-shared/src/jvmMain/kotlin/io/kotest/data/TableFile.kt index 9d5860195f0..b71a75cc716 100644 --- a/kotest-assertions/kotest-assertions-shared/src/jvmMain/kotlin/io/kotest/data/TableFile.kt +++ b/kotest-assertions/kotest-assertions-shared/src/jvmMain/kotlin/io/kotest/data/TableFile.kt @@ -39,10 +39,27 @@ fun File.writeTable(headers: List, cells: List>): String { if (extension != "table") throw AssertionError("Table file must have a .table extension") val containsNewLines = cells.any { it.any { cell -> cell.contains("\n") } } if (containsNewLines) throw AssertionError("Cells con't contain new lines") + + val columnSizes = headers.mapIndexed { index, header -> + val rowsSize = cells.map { it[index].length }.maxOrNull() ?: 0 + maxOf(header.length, rowsSize) + } + + fun String.formatCell(index: Int) = + this.plus(" ".repeat(maxOf(0, columnSizes[index] - length))) + val separator = " | " - val formattedHeader = headers.joinToString(separator) + + val formattedHeader = headers + .mapIndexed { index, header -> header.formatCell(index) } + .joinToString(separator) + .trimEnd() + val formattedContent = cells.joinToString("\n") { row -> - row.joinToString(separator) { it.replace("|", "\\|") } + val list = row.mapIndexed { index, cell -> + cell.replace("|", "\\|").formatCell(index) + } + list.joinToString(separator).trimEnd() } val fileContent = """ From 524f3abce086df19c9a749f8b941717185a3db96 Mon Sep 17 00:00:00 2001 From: Jean-Michel Fayard <459464+jmfayard@users.noreply.github.com> Date: Mon, 5 Sep 2022 17:17:24 +0200 Subject: [PATCH 15/18] =?UTF-8?q?=F0=9F=91=8Cescape=20escape,=20update=20d?= =?UTF-8?q?oc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../assertions/table_driven_testing.md | 9 ++++++- .../sksamuel/kotest/data/StringTableTest.kt | 8 ++++-- .../kotlin/io/kotest/data/StringTable.kt | 25 +++++++++++++++---- 3 files changed, 34 insertions(+), 8 deletions(-) diff --git a/documentation/versioned_docs/version-5.4/assertions/table_driven_testing.md b/documentation/versioned_docs/version-5.4/assertions/table_driven_testing.md index f10ba9fd137..302aad2c6af 100644 --- a/documentation/versioned_docs/version-5.4/assertions/table_driven_testing.md +++ b/documentation/versioned_docs/version-5.4/assertions/table_driven_testing.md @@ -50,7 +50,14 @@ id | username | fullName Curious why it's not just a .csv file? -Well CSV is not a well defined format. Everyone has its flavor and we have too. The `.table` has its rules: it always uses `|` as separator, it must have an header, it can have comments, it can't have newlines inside the columns. Basically it's optimized for putting table data in a `.table` file. +Well CSV is not a well defined format. Everyone has its flavor and we have too. The `.table` has its rules: + +- it always uses `|` as separator +- it must have an header +- cells are trimmed and cannot contain new lines +- it can have comments and blank lines + +Basically it's optimized for putting table data in a `.table` file. We hope you don't use Microsoft Excel to edit the CSV-like file. IntelliJ with the [CSV plugin from Martin Sommer](https://plugins.jetbrains.com/plugin/10037-csv) does that better. You can associate the `.table` extension with it and configure `|` as your CSV separator. It has a table edition mode too! diff --git a/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/data/StringTableTest.kt b/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/data/StringTableTest.kt index ff308b5de0d..565e63d3b4c 100644 --- a/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/data/StringTableTest.kt +++ b/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/data/StringTableTest.kt @@ -97,11 +97,15 @@ class StringTableTest : FunSpec({ test("The '|' character can be escaped") { val fileContent = """ - 1 | bad \| good | name + 1 | prefix\|middle\|suffix | hello\|world + 2 | prefix\suffix | nothing + 3 | prefix\\|suffix """.trimIndent() table(headers, fileContent, transform) shouldBe table( headers, - row(1, "bad | good", "name") + row(1, "prefix|middle|suffix", "hello|world"), + row(2,"prefix\\suffix", "nothing"), + row(3, "prefix\\", "suffix"), ) } diff --git a/kotest-assertions/kotest-assertions-shared/src/commonMain/kotlin/io/kotest/data/StringTable.kt b/kotest-assertions/kotest-assertions-shared/src/commonMain/kotlin/io/kotest/data/StringTable.kt index 2b370653d1d..21cb7bf49ad 100644 --- a/kotest-assertions/kotest-assertions-shared/src/commonMain/kotlin/io/kotest/data/StringTable.kt +++ b/kotest-assertions/kotest-assertions-shared/src/commonMain/kotlin/io/kotest/data/StringTable.kt @@ -33,11 +33,26 @@ internal data class StringTable( companion object { internal fun parseRow(line: String): List { - val notAPipeSeparator = "🫓" - return line - .replace("\\|", notAPipeSeparator) - .split("|") - .map { it.trim().replace(notAPipeSeparator, "|") } + val result = mutableListOf() + val list = line.split("|") + val needsMerge = list.withIndex().filter { (i, cell) -> + cell.endsWith("\\") && cell.endsWith("\\\\").not() + }.map { it.index }.toSet() + + var current = "" + list.forEachIndexed { i, cell -> + if (i in needsMerge) { + current += cell + .removeSuffix("\\") + .plus("|") + } else { + result += "$current$cell" + .replace("\\\\", "\\") + .trim() + current = "" + } + } + return result } } } From 74325030e7bbb170388c61ea8e384232c50a140e Mon Sep 17 00:00:00 2001 From: Jean-Michel Fayard <459464+jmfayard@users.noreply.github.com> Date: Mon, 5 Sep 2022 17:31:10 +0200 Subject: [PATCH 16/18] =?UTF-8?q?=F0=9F=91=8Cbetter=20error=20message=20in?= =?UTF-8?q?=20rowsShouldHaveSize(int)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/sksamuel/kotest/data/StringTableTest.kt | 11 +++++++++-- .../kotlin/io/kotest/data/StringTable.kt | 17 ++++++++++++++--- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/data/StringTableTest.kt b/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/data/StringTableTest.kt index 565e63d3b4c..a1decf0d6f5 100644 --- a/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/data/StringTableTest.kt +++ b/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/data/StringTableTest.kt @@ -86,11 +86,18 @@ class StringTableTest : FunSpec({ test("All rows must have the right number of columns") { val invalidRows = """ 4 | jmfayard | Jean-Michel Fayard - 5 | victor | Victor Hugo | victor.hugo@guernesey.co.k + 5 | victor | Victor Hugo | victor.hugo@guernesey.co.uk 6 | louis | Louis Caugnault 7 | edgar """.trimIndent() - shouldThrowMessage("Expected all rows to have size 3, but got rows at lines [1, 3]") { + + val expectedMessage = """ +Expected all rows to have 3 columns, but 2 rows differed +- Row 1 has 4 columns: [5, victor, Victor Hugo, victor.hugo@guernesey.co.uk] +- Row 3 has 2 columns: [7, edgar] + """.trimIndent() + + shouldThrowMessage(expectedMessage) { table(headers, invalidRows, transform) } } diff --git a/kotest-assertions/kotest-assertions-shared/src/commonMain/kotlin/io/kotest/data/StringTable.kt b/kotest-assertions/kotest-assertions-shared/src/commonMain/kotlin/io/kotest/data/StringTable.kt index 21cb7bf49ad..95d6a3249d7 100644 --- a/kotest-assertions/kotest-assertions-shared/src/commonMain/kotlin/io/kotest/data/StringTable.kt +++ b/kotest-assertions/kotest-assertions-shared/src/commonMain/kotlin/io/kotest/data/StringTable.kt @@ -25,10 +25,21 @@ internal data class StringTable( } private fun rowsShouldHaveSize(size: Int) { - val invalid = rows + val maxRows = 5 + val invalidRows = rows .filter { it.value.size != size } - .map { it.index } - if (invalid.isNotEmpty()) fail("Expected all rows to have size $size, but got rows at lines $invalid") + val formattedRows = invalidRows + .take(maxRows) + .joinToString("\n") { (i, row) -> + "- Row $i has ${row.size} columns: $row" + } + val andMore = if (invalidRows.size <= maxRows) "" else "... and ${invalidRows.size - maxRows} other rows" + + if (invalidRows.isNotEmpty()) fail(""" + |Expected all rows to have $size columns, but ${invalidRows.size} rows differed + |$formattedRows + |$andMore + """.trimMargin().trim()) } companion object { From 678ab8cc83ba9c268eb7d4dbb274f60c572866df Mon Sep 17 00:00:00 2001 From: Jean-Michel Fayard <459464+jmfayard@users.noreply.github.com> Date: Mon, 5 Sep 2022 18:02:06 +0200 Subject: [PATCH 17/18] =?UTF-8?q?=F0=9F=91=8CseparatorRegex?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/io/kotest/data/StringTable.kt | 34 ++++++++----------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/kotest-assertions/kotest-assertions-shared/src/commonMain/kotlin/io/kotest/data/StringTable.kt b/kotest-assertions/kotest-assertions-shared/src/commonMain/kotlin/io/kotest/data/StringTable.kt index 95d6a3249d7..96786cd0c95 100644 --- a/kotest-assertions/kotest-assertions-shared/src/commonMain/kotlin/io/kotest/data/StringTable.kt +++ b/kotest-assertions/kotest-assertions-shared/src/commonMain/kotlin/io/kotest/data/StringTable.kt @@ -35,35 +35,29 @@ internal data class StringTable( } val andMore = if (invalidRows.size <= maxRows) "" else "... and ${invalidRows.size - maxRows} other rows" - if (invalidRows.isNotEmpty()) fail(""" + if (invalidRows.isNotEmpty()) fail( + """ |Expected all rows to have $size columns, but ${invalidRows.size} rows differed |$formattedRows |$andMore - """.trimMargin().trim()) + """.trimMargin().trim() + ) } companion object { - internal fun parseRow(line: String): List { - val result = mutableListOf() - val list = line.split("|") - val needsMerge = list.withIndex().filter { (i, cell) -> - cell.endsWith("\\") && cell.endsWith("\\\\").not() - }.map { it.index }.toSet() + val separatorRegex = Regex("([\\\\]{2}|[^\\\\])\\|") - var current = "" - list.forEachIndexed { i, cell -> - if (i in needsMerge) { - current += cell - .removeSuffix("\\") - .plus("|") - } else { - result += "$current$cell" + internal fun parseRow(line: String): List { + val trimmed = line.replace(" ", "") + return line + .split(separatorRegex) + .map { + val cell = it.trim() + val suffix = if ("$cell\\\\|" in trimmed) "\\" else "" + cell.plus(suffix) + .replace("\\|", "|") .replace("\\\\", "\\") - .trim() - current = "" - } } - return result } } } From fd22ea11c19952b6a00f1426f2725fb28a4f680c Mon Sep 17 00:00:00 2001 From: Jean-Michel Fayard <459464+jmfayard@users.noreply.github.com> Date: Mon, 5 Sep 2022 18:54:29 +0200 Subject: [PATCH 18/18] =?UTF-8?q?=E2=9C=A8up=20to=209=20generics=20for=20t?= =?UTF-8?q?able(rows),=20table(transform)=20table.mapRows?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/io/kotest/data/tables.kt | 78 +++++++++++++++---- 1 file changed, 63 insertions(+), 15 deletions(-) diff --git a/kotest-assertions/kotest-assertions-shared/src/commonMain/kotlin/io/kotest/data/tables.kt b/kotest-assertions/kotest-assertions-shared/src/commonMain/kotlin/io/kotest/data/tables.kt index 4ec4ade013a..37596b6aeda 100644 --- a/kotest-assertions/kotest-assertions-shared/src/commonMain/kotlin/io/kotest/data/tables.kt +++ b/kotest-assertions/kotest-assertions-shared/src/commonMain/kotlin/io/kotest/data/tables.kt @@ -7,12 +7,22 @@ fun Map.toTable( fun table(headers: Headers1, rows: List>) = Table1(headers, rows) fun table(headers: Headers2, rows: List>) = Table2(headers, rows) fun table(headers: Headers3, rows: List>) = Table3(headers, rows) -// TODO more +fun table(headers: Headers4, rows: List>) = Table4(headers, rows) +fun table(headers: Headers5, rows: List>) = Table5(headers, rows) +fun table(headers: Headers6, rows: List>) = Table6(headers, rows) +fun table(headers: Headers7, rows: List>) = Table7(headers, rows) +fun table(headers: Headers8, rows: List>) = Table8(headers, rows) +fun table(headers: Headers9, rows: List>) = Table9(headers, rows) -fun Table1.mapRows(fn: (Row1) -> T): List = rows.map(fn) -fun Table2.mapRows(fn: (Row2) -> T): List = rows.map(fn) -fun Table3.mapRows(fn: (Row3) -> T): List = rows.map(fn) -// TODO more +fun Table1.mapRows(fn: (Row1) -> ARow): List = rows.map(fn) +fun Table2.mapRows(fn: (Row2) -> ARow): List = rows.map(fn) +fun Table3.mapRows(fn: (Row3) -> ARow): List = rows.map(fn) +fun Table4.mapRows(fn: (Row4) -> ARow): List = rows.map(fn) +fun Table5.mapRows(fn: (Row5) -> ARow): List = rows.map(fn) +fun Table6.mapRows(fn: (Row6) -> ARow): List = rows.map(fn) +fun Table7.mapRows(fn: (Row7) -> ARow): List = rows.map(fn) +fun Table8.mapRows(fn: (Row8) -> ARow): List = rows.map(fn) +fun Table9.mapRows(fn: (Row9) -> ARow): List = rows.map(fn) fun table(headers: Headers1, vararg rows: Row1) = Table1(headers, rows.asList()) fun table(headers: Headers2, vararg rows: Row2) = Table2(headers, rows.asList()) @@ -60,14 +70,52 @@ data class Table20(val headers: Headers21, val rows: List>) data class Table22(val headers: Headers22, val rows: List>) -// TODO I'm only supporting table with 3 arguments until the API stabilizes -fun table( - headers: Headers3, - fileContent: String, - transform: (String, String, String) -> Row3 -): Table3 { - val table = StringTable(headers.values(), fileContent.lines()) - val rows = table.mapRows { (a, b, c) -> transform(a, b, c) } - return Table3(headers, rows) -} +fun table(headers: Headers1, fileContent: String, transform: (String) -> Row1) = Table1( + headers = headers, + rows = StringTable(headers.values(), fileContent.lines()) + .mapRows { transform(it[0]) } +) +fun table(headers: Headers2, fileContent: String, transform: (String, String) -> Row2) = Table2( + headers = headers, + rows = StringTable(headers.values(), fileContent.lines()) + .mapRows { transform(it[0], it[1]) } +) + +fun table(headers: Headers3, fileContent: String, transform:(String, String, String) -> Row3 +): Table3 = Table3( + headers = headers, + rows = StringTable(headers.values(), fileContent.lines()) + .mapRows { transform(it[0], it[1], it[2]) } +) + +fun table(headers: Headers4, fileContent: String, transform:(String, String, String, String) -> Row4) = Table4( + headers = headers, + rows = StringTable(headers.values(), fileContent.lines()) + .mapRows { transform(it[0], it[1], it[2], it[3])} +) +fun table(headers: Headers5, fileContent: String, transform:(String, String, String, String, String) -> Row5) = Table5( + headers = headers, + rows = StringTable(headers.values(), fileContent.lines()) + .mapRows { transform(it[0], it[1], it[2], it[3], it[4])} +) +fun table(headers: Headers6, fileContent: String, transform:(String, String, String, String, String, String) -> Row6) = Table6( + headers = headers, + rows = StringTable(headers.values(), fileContent.lines()) + .mapRows { transform(it[0], it[1], it[2], it[3], it[4],it[5]) } +) +fun table(headers: Headers7, fileContent: String, transform:(String, String, String, String, String, String, String) -> Row7) = Table7( + headers = headers, + rows = StringTable(headers.values(), fileContent.lines()) + .mapRows { transform(it[0], it[1], it[2], it[3], it[4],it[5], it[6])} +) +fun table(headers: Headers8, fileContent: String, transform:(String, String, String, String, String, String, String, String) -> Row8) = Table8( + headers = headers, + rows = StringTable(headers.values(), fileContent.lines()) + .mapRows { transform(it[0], it[1], it[2], it[3], it[4],it[5], it[6], it[7])} +) +fun table(headers: Headers9, fileContent: String, transform:(String, String, String, String, String, String, String, String, String) -> Row9) = Table9( + headers = headers, + rows = StringTable(headers.values(), fileContent.lines()) + .mapRows { transform(it[0], it[1], it[2], it[3], it[4],it[5], it[6], it[7], it[8])} +)