Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# Changelog

## 0.15.0
* **Matrix testing:**
* `data` now accepts a `Map<K, V>` directly, exposing each entry as a destructurable `Pair<K, V>`
(`data("envs", mapOf("dev" to 8080)) - { (name, port) -> … }`). Default name is `"<index>: (key: value)"`.
Entries iterate in map order, so use an ordered map (the `mapOf`/`linkedMapOf` default) for reproducible
`replay`.
* Fluent receiver form `.asData(…)` on `Iterable`, `Sequence`, and `Map`, mirroring `data` one-to-one:
`listOf(1, 2, 3).asData() - { … }`, `myMap.asData(nameFn = { (k, _) -> k }) test { … }`.
* `nameFn` now resolves by lambda arity: a single-parameter namer (`{ v -> … }`, `{ it }`, or destructured
`{ (k, v) -> … }`) names cases by value alone; the two-parameter `{ index, value -> … }` form keeps the index;
omitting it uses the indexed default. Applies to `data`, `.asData`, and `property`.

## 0.14.0
* **Matrix testing:**
* **Breaking:** `matrixSuite(...)` now takes its configuration as a single `matrixConfig { … }` argument instead of
Expand Down
15 changes: 13 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,19 @@ nameless overload.

### Data-Driven Matrix Layers

`data` layers accept `Iterable` values and lazy `Sequence` values. Use `test { ... }` when each row is the test, and
`- { ... }` when each row is a dimension that contains more layers or explicit tests.
`data` layers accept `Iterable` values, lazy `Sequence` values, and a `Map` (each entry comes through as a
destructurable `Pair<K, V>`, named `"<index>: (key: value)"` by default). Use `test { ... }` when each row is the test,
and `- { ... }` when each row is a dimension that contains more layers or explicit tests.

Every collection also reads fluently as a receiver via `.asData(…)`, with the exact same options as `data`:

```kotlin
listOf(1, 2, 3).asData() test { it shouldBeGreaterThan 0 }
mapOf("dev" to 8080, "prod" to 443).asData(nameFn = { (env, _) -> env }) - { (env, port) -> /* … */ }
```

`nameFn` resolves by lambda arity: a single-parameter namer (`{ v -> … }`, `{ it }`, or destructured `{ (k, v) -> … }`)
names by value alone, while the two-parameter `{ index, value -> … }` form keeps the index.

```kotlin
val dataDrivenMatrix by matrixSuite(matrixConfig { execution = ExecutionMode.Sequential }) {
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ kotlin.code.style=official
org.gradle.jvmargs=-Xmx10g -Dfile.encoding=UTF-8 -Xms200m
kotlin.daemon.jvm.options=-Xmx10G -Xms200m
kotlin.daemon.jvmargs=-Xmx10g -Xms200m
artifactVersion = 0.14.0
artifactVersion = 0.15.0
jdk.version=17
android.minSdk=21
android.compileSdk=34
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ import kotlinx.coroutines.sync.Semaphore
internal fun defaultLayerName(index: Long, value: Any?): String =
"${index}: ${value.toPrettyString()}"

/** Default name for a map-backed data layer case: the entry rendered as `($key: $value)`. */
internal fun <K, V> defaultMapEntryName(index: Long, entry: Pair<K, V>): String =
"${index}: (${entry.first.toPrettyString()}: ${entry.second.toPrettyString()})"

/**
* One generated/enumerated case of a matrix layer, carrying its original (authoritative) index and,
* for property layers, the [seed] of the random source that produced it (`null` for data layers).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,14 @@ sealed interface MatrixScope<SELF : MatrixScope<SELF>> {
*/
private fun self(): SELF = @Suppress("UNCHECKED_CAST") (this as SELF)

// --- `data`: named / nameless × Iterable / Sequence. `replay = Indexes(...)` pins specific case indexes. ---
// --- `data`: named / nameless × Iterable / Sequence / Map. `replay = Indexes(...)` pins specific case indexes.
// A `Map` is exposed case-by-case as a destructurable `Pair<K, V>` (entries materialized in iteration order);
// replay indexes are positional, so use an ordered map (the `mapOf` / `linkedMapOf` default) for reproducible
// replay. Every collection also reads fluently via the `.asData(...)` receiver form below.
//
// `nameFn` resolves by lambda arity: a single-param namer (`{ v -> }`, `{ it }`, or destructured
// `{ (k, v) -> }`) names cases by value alone; the two-param `{ index, value -> }` form keeps the index;
// omitting it uses the indexed default (`defaultLayerName` / `defaultMapEntryName`). ---

@TestRegistering
fun <T> data(
Expand All @@ -31,6 +38,15 @@ sealed interface MatrixScope<SELF : MatrixScope<SELF>> {
config: DataLayerConfigBuilder.() -> Unit = {},
): DataLayer<T, SELF> = DataLayer(self(), name, IterableDataSource(values), nameFn, replay, config)

@TestRegistering
fun <T> data(
name: String,
values: Iterable<T>,
nameFn: (value: T) -> String,
replay: Indexes? = null,
config: DataLayerConfigBuilder.() -> Unit = {},
): DataLayer<T, SELF> = data(name, values, { _, v -> nameFn(v) }, replay, config)

@TestRegistering
fun <T> data(
name: String,
Expand All @@ -41,13 +57,49 @@ sealed interface MatrixScope<SELF : MatrixScope<SELF>> {
config: DataLayerConfigBuilder.() -> Unit = {},
): DataLayer<T, SELF> = DataLayer(self(), name, SequenceDataSource(values, limit), nameFn, replay, config)

@TestRegistering
fun <T> data(
name: String,
values: Sequence<T>,
nameFn: (value: T) -> String,
limit: Long? = null,
replay: Indexes? = null,
config: DataLayerConfigBuilder.() -> Unit = {},
): DataLayer<T, SELF> = data(name, values, limit, { _, v -> nameFn(v) }, replay, config)

@TestRegistering
fun <K, V> data(
name: String,
values: Map<K, V>,
nameFn: NameFn<Pair<K, V>> = ::defaultMapEntryName,
replay: Indexes? = null,
config: DataLayerConfigBuilder.() -> Unit = {},
): DataLayer<Pair<K, V>, SELF> =
DataLayer(self(), name, IterableDataSource(values.entries.map { it.toPair() }), nameFn, replay, config)

@TestRegistering
fun <K, V> data(
name: String,
values: Map<K, V>,
nameFn: (entry: Pair<K, V>) -> String,
replay: Indexes? = null,
config: DataLayerConfigBuilder.() -> Unit = {},
): DataLayer<Pair<K, V>, SELF> = data(name, values, { _, e -> nameFn(e) }, replay, config)

fun <T> data(
values: Iterable<T>,
nameFn: NameFn<T> = ::defaultLayerName,
replay: Indexes? = null,
config: DataLayerConfigBuilder.() -> Unit = {},
): DataLayer<T, SELF> = DataLayer(self(), null, IterableDataSource(values), nameFn, replay, config)

fun <T> data(
values: Iterable<T>,
nameFn: (value: T) -> String,
replay: Indexes? = null,
config: DataLayerConfigBuilder.() -> Unit = {},
): DataLayer<T, SELF> = data(values, { _, v -> nameFn(v) }, replay, config)

fun <T> data(
values: Sequence<T>,
limit: Long? = null,
Expand All @@ -56,7 +108,124 @@ sealed interface MatrixScope<SELF : MatrixScope<SELF>> {
config: DataLayerConfigBuilder.() -> Unit = {},
): DataLayer<T, SELF> = DataLayer(self(), null, SequenceDataSource(values, limit), nameFn, replay, config)

// --- `property`: named / nameless. `replay = Cases(seed = .., iter = ..)` pins recorded cases. ---
fun <T> data(
values: Sequence<T>,
nameFn: (value: T) -> String,
limit: Long? = null,
replay: Indexes? = null,
config: DataLayerConfigBuilder.() -> Unit = {},
): DataLayer<T, SELF> = data(values, limit, { _, v -> nameFn(v) }, replay, config)

fun <K, V> data(
values: Map<K, V>,
nameFn: NameFn<Pair<K, V>> = ::defaultMapEntryName,
replay: Indexes? = null,
config: DataLayerConfigBuilder.() -> Unit = {},
): DataLayer<Pair<K, V>, SELF> =
DataLayer(self(), null, IterableDataSource(values.entries.map { it.toPair() }), nameFn, replay, config)

fun <K, V> data(
values: Map<K, V>,
nameFn: (entry: Pair<K, V>) -> String,
replay: Indexes? = null,
config: DataLayerConfigBuilder.() -> Unit = {},
): DataLayer<Pair<K, V>, SELF> = data(values, { _, e -> nameFn(e) }, replay, config)

// --- `.asData(...)`: fluent receiver form. Each overload just forwards to the matching `data(...)` above, so
// layer construction and the single-param→`NameFn` adaptation live in exactly one place (`data`). The
// named/nameless × two-/single-param pairing mirrors `data` and is required for `nameFn` arity resolution:
// Kotlin can't accept both `{ v -> }` and `{ i, v -> }` through one parameter. ---

@TestRegistering
fun <T> Iterable<T>.asData(
name: String,
nameFn: NameFn<T> = ::defaultLayerName,
replay: Indexes? = null,
config: DataLayerConfigBuilder.() -> Unit = {},
): DataLayer<T, SELF> = data(name, this, nameFn, replay, config)

@TestRegistering
fun <T> Iterable<T>.asData(
name: String,
nameFn: (value: T) -> String,
replay: Indexes? = null,
config: DataLayerConfigBuilder.() -> Unit = {},
): DataLayer<T, SELF> = data(name, this, nameFn, replay, config)

fun <T> Iterable<T>.asData(
nameFn: NameFn<T> = ::defaultLayerName,
replay: Indexes? = null,
config: DataLayerConfigBuilder.() -> Unit = {},
): DataLayer<T, SELF> = data(this, nameFn, replay, config)

fun <T> Iterable<T>.asData(
nameFn: (value: T) -> String,
replay: Indexes? = null,
config: DataLayerConfigBuilder.() -> Unit = {},
): DataLayer<T, SELF> = data(this, nameFn, replay, config)

@TestRegistering
fun <T> Sequence<T>.asData(
name: String,
limit: Long? = null,
nameFn: NameFn<T> = ::defaultLayerName,
replay: Indexes? = null,
config: DataLayerConfigBuilder.() -> Unit = {},
): DataLayer<T, SELF> = data(name, this, limit, nameFn, replay, config)

@TestRegistering
fun <T> Sequence<T>.asData(
name: String,
nameFn: (value: T) -> String,
limit: Long? = null,
replay: Indexes? = null,
config: DataLayerConfigBuilder.() -> Unit = {},
): DataLayer<T, SELF> = data(name, this, nameFn, limit, replay, config)

fun <T> Sequence<T>.asData(
limit: Long? = null,
nameFn: NameFn<T> = ::defaultLayerName,
replay: Indexes? = null,
config: DataLayerConfigBuilder.() -> Unit = {},
): DataLayer<T, SELF> = data(this, limit, nameFn, replay, config)

fun <T> Sequence<T>.asData(
nameFn: (value: T) -> String,
limit: Long? = null,
replay: Indexes? = null,
config: DataLayerConfigBuilder.() -> Unit = {},
): DataLayer<T, SELF> = data(this, nameFn, limit, replay, config)

@TestRegistering
fun <K, V> Map<K, V>.asData(
name: String,
nameFn: NameFn<Pair<K, V>> = ::defaultMapEntryName,
replay: Indexes? = null,
config: DataLayerConfigBuilder.() -> Unit = {},
): DataLayer<Pair<K, V>, SELF> = data(name, this, nameFn, replay, config)

@TestRegistering
fun <K, V> Map<K, V>.asData(
name: String,
nameFn: (entry: Pair<K, V>) -> String,
replay: Indexes? = null,
config: DataLayerConfigBuilder.() -> Unit = {},
): DataLayer<Pair<K, V>, SELF> = data(name, this, nameFn, replay, config)

fun <K, V> Map<K, V>.asData(
nameFn: NameFn<Pair<K, V>> = ::defaultMapEntryName,
replay: Indexes? = null,
config: DataLayerConfigBuilder.() -> Unit = {},
): DataLayer<Pair<K, V>, SELF> = data(this, nameFn, replay, config)

fun <K, V> Map<K, V>.asData(
nameFn: (entry: Pair<K, V>) -> String,
replay: Indexes? = null,
config: DataLayerConfigBuilder.() -> Unit = {},
): DataLayer<Pair<K, V>, SELF> = data(this, nameFn, replay, config)

// --- `property`: named / nameless. `replay = Cases(seed = .., iter = ..)` pins recorded cases. `nameFn`
// resolves by lambda arity as for `data` (single param = value-only, two params = `index, value`). ---

@TestRegistering
fun <T> property(
Expand All @@ -68,13 +237,31 @@ sealed interface MatrixScope<SELF : MatrixScope<SELF>> {
config: PropertyLayerConfigBuilder.() -> Unit = {},
): PropertyLayer<T, SELF> = PropertyLayer(self(), name, gen, iterations, nameFn, replay, config)

@TestRegistering
fun <T> property(
name: String,
gen: Gen<T>,
nameFn: (value: T) -> String,
iterations: Int = matrixConfig.defaultPropertyIterations,
replay: Cases? = null,
config: PropertyLayerConfigBuilder.() -> Unit = {},
): PropertyLayer<T, SELF> = property(name, gen, iterations, { _, v -> nameFn(v) }, replay, config)

fun <T> property(
gen: Gen<T>,
iterations: Int = matrixConfig.defaultPropertyIterations,
nameFn: NameFn<T> = ::defaultLayerName,
replay: Cases? = null,
config: PropertyLayerConfigBuilder.() -> Unit = {},
): PropertyLayer<T, SELF> = PropertyLayer(self(), null, gen, iterations, nameFn, replay, config)

fun <T> property(
gen: Gen<T>,
nameFn: (value: T) -> String,
iterations: Int = matrixConfig.defaultPropertyIterations,
replay: Cases? = null,
config: PropertyLayerConfigBuilder.() -> Unit = {},
): PropertyLayer<T, SELF> = property(gen, iterations, { _, v -> nameFn(v) }, replay, config)
}

// --- Layer builders returned by `data` / `property`; `- { }` opens a container, `test { }` a terminal. ---
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,30 @@ val MatrixDataSourceTest by testSuite {
IterableDataSource(listOf(10, 20, 30)).cases(listOf(1L, 1L)).asSequence().toList()
.map { it.value } shouldBe listOf(20)
}

// --- Map layers materialize entries as `Pair<K, V>` in iteration order, then reuse the iterable machinery. ---

test("map-backed source reports the map size") {
val map = linkedMapOf("a" to 1, "b" to 2, "c" to 3)
IterableDataSource(map.entries.map { it.toPair() }).knownSize shouldBe 3L
}

test("map-backed cases preserve insertion order with ascending indexes") {
val map = linkedMapOf("a" to 1, "b" to 2)
val cases = IterableDataSource(map.entries.map { it.toPair() }).cases(null).asSequence().toList()
cases.map { it.index } shouldBe listOf(0L, 1L)
cases.map { it.value } shouldBe listOf("a" to 1, "b" to 2)
}

test("default map entry name renders index and (key: value)") {
defaultMapEntryName(0L, "a" to 1) shouldBe "0: (a: 1)"
defaultMapEntryName(2L, "host" to "prod") shouldBe "2: (host: prod)"
}

test("map-backed replay indexes select entries by position") {
val map = linkedMapOf("a" to 10, "b" to 20, "c" to 30, "d" to 40)
val cases = IterableDataSource(map.entries.map { it.toPair() }).cases(listOf(3L, 0L)).asSequence().toList()
cases.map { it.index } shouldBe listOf(0L, 3L)
cases.map { it.value } shouldBe listOf("a" to 10, "d" to 40)
}
}
Loading
Loading