diff --git a/README.md b/README.md index 346e716b..54882dc8 100644 --- a/README.md +++ b/README.md @@ -39,12 +39,13 @@ val user = some() ## Installation -Some is published as four artifacts: +Some is published as five artifacts: - `some-core` for Java and Kotlin/JVM projects - `some-android` for Android projects. It re-exports the core API, so you do not need to add `some-core` separately. - `some-kotest` for Kotest `Arb` integration. Add it alongside either `some-core` or `some-android`. - `some-retrofit` for Retrofit response fixtures. Add it alongside `some-core` and Retrofit. +- `some-kotlin-fixture` for Appmattus KotlinFixture compatibility and migration. ```kotlin dependencies { @@ -61,6 +62,9 @@ dependencies { // Optional: Retrofit response integration. // Retrofit must be declared directly by the consuming project. testImplementation("dev.appoutlet:some-retrofit:{version}") + + // Optional: Appmattus KotlinFixture compatibility. For better migration + testImplementation("dev.appoutlet:some-kotlin-fixture:{version}") } ``` diff --git a/build.gradle.kts b/build.gradle.kts index a3c78e5b..0670ba05 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -21,6 +21,7 @@ dependencies { dokka(projects.core) dokka(projects.kotest) dokka(projects.retrofit) + dokka(projects.kotlinFixture) } tasks.named("prepareKotlinBuildScriptModel") { diff --git a/core/src/main/kotlin/dev/appoutlet/some/config/CollectionStrategy.kt b/core/src/main/kotlin/dev/appoutlet/some/config/CollectionStrategy.kt index 550336a9..82f362ac 100644 --- a/core/src/main/kotlin/dev/appoutlet/some/config/CollectionStrategy.kt +++ b/core/src/main/kotlin/dev/appoutlet/some/config/CollectionStrategy.kt @@ -4,6 +4,14 @@ private const val DEFAULT_SIZE_RANGE_START = 1 private const val DEFAULT_SIZE_RANGE_END = 5 private val defaultSizeRange = DEFAULT_SIZE_RANGE_START..DEFAULT_SIZE_RANGE_END +/** + * Strategy controlling the number of values generated for collections and maps. + * + * The configured range is inclusive at both ends. The default generates between one and five values. + * + * @property sizeRange Inclusive range of collection sizes. Both bounds must be non-negative and the end must not be + * less than the start. + */ data class CollectionStrategy( val sizeRange: IntRange = defaultSizeRange ) : Strategy { @@ -11,9 +19,16 @@ data class CollectionStrategy( init { require(sizeRange.first > -1) { "sizeRange.start must be positive" } - require(sizeRange.last > sizeRange.first) { "sizeRange.end must be greater than or equal to sizeRange.start" } + require(sizeRange.last >= sizeRange.first) { "sizeRange.end must be greater than or equal to sizeRange.start" } } + /** + * Creates a strategy that always generates collections with [size] values. + * + * @param size Fixed non-negative collection size. + */ + constructor(size: Int) : this(size..size) + companion object { /** * The default collection strategy. diff --git a/core/src/main/kotlin/dev/appoutlet/some/config/DefaultValueStrategy.kt b/core/src/main/kotlin/dev/appoutlet/some/config/DefaultValueStrategy.kt index b2e4fc2f..40f47bd7 100644 --- a/core/src/main/kotlin/dev/appoutlet/some/config/DefaultValueStrategy.kt +++ b/core/src/main/kotlin/dev/appoutlet/some/config/DefaultValueStrategy.kt @@ -9,6 +9,7 @@ package dev.appoutlet.some.config * * - [UseDefault] – (Default) Uses the Kotlin default value for optional parameters. * - [Generate] – Generates a value for optional parameters through the resolver chain. + * - [Random] – Randomly uses the Kotlin default or generates a value according to a probability. * * ## Example Usage * @@ -38,6 +39,17 @@ sealed interface DefaultValueStrategy : Strategy { */ data object Generate : DefaultValueStrategy + /** + * Generates a value for optional parameters with a specified probability using the shared random source. + * + * @property probability The probability (between 0.0 and 1.0) that an optional parameter will be generated. + */ + data class Random(val probability: Float = 0.5f) : DefaultValueStrategy { + init { + require(probability in 0f..1f) { "Probability must be between 0.0 and 1.0" } + } + } + companion object { /** * The default default-value strategy. diff --git a/core/src/main/kotlin/dev/appoutlet/some/resolver/ClassResolver.kt b/core/src/main/kotlin/dev/appoutlet/some/resolver/ClassResolver.kt index 26cc2155..4c4f1b60 100644 --- a/core/src/main/kotlin/dev/appoutlet/some/resolver/ClassResolver.kt +++ b/core/src/main/kotlin/dev/appoutlet/some/resolver/ClassResolver.kt @@ -139,8 +139,13 @@ class ClassResolver( ): Any? { val args = constructor.parameters.mapNotNull { param -> val propertyFactory = propertyFactories[kClass to param.name] - val shouldGenerate = !param.isOptional || - defaultValueStrategy == DefaultValueStrategy.Generate + val shouldGenerate = when { + !param.isOptional -> true + defaultValueStrategy == DefaultValueStrategy.Generate -> true + defaultValueStrategy is DefaultValueStrategy.Random -> + random.nextFloat() < defaultValueStrategy.probability + else -> false + } when { propertyFactory != null -> resolveByPropertyFactory(chain, param, propertyFactory) diff --git a/core/src/test/kotlin/dev/appoutlet/some/config/CollectionStrategyTest.kt b/core/src/test/kotlin/dev/appoutlet/some/config/CollectionStrategyTest.kt index b62bb022..a053e2c0 100644 --- a/core/src/test/kotlin/dev/appoutlet/some/config/CollectionStrategyTest.kt +++ b/core/src/test/kotlin/dev/appoutlet/some/config/CollectionStrategyTest.kt @@ -35,11 +35,18 @@ class CollectionStrategyTest { } @Test - fun `CollectionStrategy rejects range where end equals start`() { - val exception = assertFailsWith { - CollectionStrategy(5..5) - } - assertEquals("sizeRange.end must be greater than or equal to sizeRange.start", exception.message) + fun `CollectionStrategy accepts range where end equals start`() { + val strategy = CollectionStrategy(5..5) + + assertEquals(5, strategy.sizeRange.first) + assertEquals(5, strategy.sizeRange.last) + } + + @Test + fun `CollectionStrategy accepts a fixed size`() { + val strategy = CollectionStrategy(5) + + assertEquals(5..5, strategy.sizeRange) } @Suppress("InvalidRange") diff --git a/core/src/test/kotlin/dev/appoutlet/some/integration/DefaultValueStrategyIntegrationTest.kt b/core/src/test/kotlin/dev/appoutlet/some/integration/DefaultValueStrategyIntegrationTest.kt index 033c829d..62caf171 100644 --- a/core/src/test/kotlin/dev/appoutlet/some/integration/DefaultValueStrategyIntegrationTest.kt +++ b/core/src/test/kotlin/dev/appoutlet/some/integration/DefaultValueStrategyIntegrationTest.kt @@ -38,6 +38,24 @@ class DefaultValueStrategyIntegrationTest { assertNotEquals("default value", result.optional) } + @Test + fun `should use default value when strategy is Random with probability 0_0`() { + val result: OptionalData = some { + strategy(DefaultValueStrategy.Random(probability = 0.0f)) + } + + assertEquals("default value", result.optional) + } + + @Test + fun `should generate value when strategy is Random with probability 1_0`() { + val result: OptionalData = some { + strategy(DefaultValueStrategy.Random(probability = 1.0f)) + } + + assertNotEquals("default value", result.optional) + } + @Test fun `property factory should take precedence over UseDefault`() { val result: OptionalData = some { diff --git a/docs/index.md b/docs/index.md index 80d6c681..e98c784f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -70,12 +70,13 @@ Writing tests means creating test data — lots of it. Constructing data classes ## Modules -Some is published as four user-facing artifacts: +Some is published as five user-facing artifacts: - `some-core` for Java and Kotlin/JVM projects. - `some-android` for Android projects. It re-exports the shared core API, so you do not need to add `some-core` separately. - `some-kotest` for Kotest `Arb` integration. Add it alongside either `some-core` or `some-android`. - `some-retrofit` for Retrofit response fixtures. Add it alongside `some-core` and Retrofit. +- `some-kotlin-fixture` for Appmattus KotlinFixture compatibility and migration. Use [Getting Started](getting-started.md) to install Some and learn the shared API. diff --git a/docs/migration/index.md b/docs/migration/index.md index c3b9afb5..f467c471 100644 --- a/docs/migration/index.md +++ b/docs/migration/index.md @@ -8,3 +8,4 @@ Use the guide that matches the exact version you are upgrading from. ## Available migrations - [0.2.1 to 0.2.2](0.2.1-to-0.2.2.md) - moves the old `some` dependency to `some-core` and introduces `some-android` +- [Appmattus KotlinFixture Migration](kotlin-fixture-to-some.md) - migrate from Appmattus KotlinFixture to Some using `some-kotlin-fixture` diff --git a/docs/migration/kotlin-fixture-to-some.md b/docs/migration/kotlin-fixture-to-some.md new file mode 100644 index 00000000..8bc6d72c --- /dev/null +++ b/docs/migration/kotlin-fixture-to-some.md @@ -0,0 +1,193 @@ +# Migrating from Appmattus KotlinFixture to Some + +`some-kotlin-fixture` provides a small compatibility API for projects migrating from Appmattus KotlinFixture. It keeps the `kotlinFixture()` entry point and fixture invocation style, while delegating configuration and generation to Some's `SomeConfigBuilder`. + +The compatibility module is intentionally thin. Configure factories, properties, strategies, and seeds with the core Some API rather than the removed KotlinFixture-specific configuration types. + +## Dependency + +Replace the Appmattus dependency with `some-kotlin-fixture`: + +=== "Gradle Kotlin DSL" + + ```kotlin + dependencies { + // Remove: + // implementation("com.appmattus.fixture:fixture:1.2.0") + + implementation("dev.appoutlet:some-kotlin-fixture:0.4.0") + } + ``` + +=== "Gradle Groovy DSL" + + ```groovy + dependencies { + // Remove: + // implementation 'com.appmattus.fixture:fixture:1.2.0' + + implementation 'dev.appoutlet:some-kotlin-fixture:0.4.0' + } + ``` + +=== "Maven" + + ```xml + + dev.appoutlet + some-kotlin-fixture + 0.4.0 + + ``` + +## Imports + +Replace the Appmattus import with the compatibility entry point and sequence strategy: + +```kotlin +// Remove: +import com.appmattus.kotlinfixture.kotlinFixture + +// Add: +import dev.appoutlet.some.compat.kotlinfixture.SequenceStrategy +import dev.appoutlet.some.compat.kotlinfixture.kotlinFixture +``` + +Some configuration types come from the core module: + +```kotlin +import dev.appoutlet.some.config.DefaultValueStrategy +import dev.appoutlet.some.config.NullableStrategy +import dev.appoutlet.some.config.StringStrategy +``` + +## Basic Generation + +The fixture remains callable and supports selecting from a non-empty range: + +```kotlin +val fixture = kotlinFixture() + +val user: User = fixture() +val age: Int = fixture(18..65) +val name: String = fixture(listOf("Alice", "Bob", "Charlie")) +``` + +An empty range falls back to generated data. + +## Configuration + +The configuration lambda is a `SomeConfigBuilder` lambda. Type factories receive a `KClass` explicitly: + +```kotlin +val fixture = kotlinFixture { + factory(String::class) { "fixed-value" } + factory(User::class) { + User( + id = random.nextInt(1, 101), + name = "generated-by-factory" + ) + } +} +``` + +Factory lambdas receive a `FixtureContext`. It exposes `random`, `resolutionStack`, and `strategyProvider`. + +Property factories use a property reference and override constructor property values: + +```kotlin +val fixture = kotlinFixture { + property(User::name) { "OverriddenName" } + property(User::age) { 30 } +} +``` + +## Per-Call Overrides + +Pass a builder lambda to an individual fixture invocation. The override applies only to that call: + +```kotlin +val fixture = kotlinFixture { + factory(String::class) { "default" } +} + +val overridden: String = fixture { + factory(String::class) { "override" } +} + +val unchanged: String = fixture() +``` + +`Fixture.create` also accepts a one-off builder lambda: + +```kotlin +val value: Int = fixture.create { + factory(Int::class) { 42 } +} +``` + +## New Fixtures + +`Fixture.new` creates a new fixture from the supplied builder lambda: + +```kotlin +val base = kotlinFixture { + factory(String::class) { "base" } +} + +val other = base.new { + factory(Int::class) { 42 } +} +``` + +The new fixture uses the configuration passed to `new`; it does not merge the original fixture's configuration. + +## Sequences + +Use `SequenceStrategy.Bounded` for a finite sequence or `SequenceStrategy.Unbounded` for a lazy infinite sequence: + +```kotlin +val batch = fixture + .asSequence(SequenceStrategy.Bounded(10)) + .toList() + +val infinite = fixture + .asSequence(SequenceStrategy.Unbounded) + .take(10) + .toList() +``` + +`SequenceStrategy.Bounded(0)` produces an empty sequence. Negative bounds are rejected. + +## Seeds and Strategies + +Use `seed` for reproducible generation: + +```kotlin +val fixture = kotlinFixture { + seed = 12345L +} +``` + +Register Some strategies directly with `strategy`: + +```kotlin +val fixture = kotlinFixture { + strategy(StringStrategy.Readable) + strategy(NullableStrategy.NeverNull) + strategy(DefaultValueStrategy.Generate) +} +``` + +## Unsupported KotlinFixture APIs + +The simplified compatibility module does not provide the former KotlinFixture-specific APIs: + +- `Configuration` and `ConfigurationBuilder` +- `NullabilityStrategy`, `OptionalStrategy`, and `RecursionStrategy` aliases +- `filter` and distinct-value configuration +- `subType` mappings +- the `range` helper inside factories +- KotlinFixture plugin integrations such as Java Faker, Generex, Kotest, and Android + +Use the corresponding core Some API where one exists. For unsupported behavior, configure a `factory` or generate the value directly in a test-specific helper. diff --git a/kotlin-fixture/build.gradle.kts b/kotlin-fixture/build.gradle.kts new file mode 100644 index 00000000..45d3fcad --- /dev/null +++ b/kotlin-fixture/build.gradle.kts @@ -0,0 +1,68 @@ +plugins { + alias(libs.plugins.detekt) + alias(libs.plugins.dokka) + alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.mavenPublish) +} + +group = rootProject.group +version = rootProject.version + +repositories { + mavenCentral() +} + +dependencies { + implementation(projects.core) + implementation(libs.kotlin.reflect) + + testImplementation(libs.kotlin.test) + + detektPlugins(libs.detekt.formatting) +} + +kotlin { + jvmToolchain(17) +} + +tasks.test { + useJUnitPlatform() +} + +mavenPublishing { + publishToMavenCentral(automaticRelease = true) + signAllPublications() + + coordinates(artifactId = "some-kotlin-fixture") + + pom { + name.set("Some KotlinFixture Compatibility") + description.set("KotlinFixture compatibility module for Some, a Kotlin test data generation library.") + inceptionYear.set("2026") + url.set("https://github.com/MessiasLima/Some") + + licenses { + license { + name.set("The Apache License, Version 2.0") + url.set("https://www.apache.org/licenses/LICENSE-2.0.txt") + distribution.set("repo") + } + } + + developers { + developer { + id.set("MessiasLima") + name.set("Messias Lima") + url.set("https://github.com/MessiasLima") + } + } + + scm { + url.set("https://github.com/MessiasLima/Some") + connection.set("scm:git:git://github.com/MessiasLima/Some.git") + developerConnection.set("scm:git:ssh://git@github.com/MessiasLima/Some.git") + } + } +} + +detekt { autoCorrect = true } diff --git a/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/Fixture.kt b/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/Fixture.kt new file mode 100644 index 00000000..8dde97b0 --- /dev/null +++ b/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/Fixture.kt @@ -0,0 +1,115 @@ +package dev.appoutlet.some.compat.kotlinfixture + +import dev.appoutlet.some.config.SomeConfigBuilder +import dev.appoutlet.some.some + +/** + * Main compatibility generator instance for the KotlinFixture compatibility API. + * + * The configuration supplied to this fixture is applied to generated values and sequences. A configuration passed to + * an individual call is applied after the fixture configuration and can override it. + * + * @param someConfig Some configuration applied to every value generated by [invoke] and [asSequence]. + */ +class Fixture @JvmOverloads constructor(val someConfig: SomeConfigBuilder.() -> Unit = {}) { + + /** + * Generates a value of type [T]. + * + * When [range] is not empty, one of its values is returned. Otherwise, Some generates a value using this fixture's + * configuration followed by [configuration]. + * + * @param T Type of value to generate. + * @param range Optional values to choose from before generating a value. + * @param configuration Per-call Some configuration applied after [someConfig]. + * @return A generated value of type [T], or a value selected from [range]. + */ + inline operator fun invoke( + range: Iterable = emptyList(), + noinline configuration: SomeConfigBuilder.() -> Unit = {} + ): T { + val rangeShuffled = range.shuffled() + return if (rangeShuffled.isNotEmpty()) { + rangeShuffled.first() + } else { + some { + this.apply(someConfig) + this.apply(configuration) + } + } + } + + /** + * Creates a sequence of values of type [T]. + * + * The sequence applies this fixture's configuration and [configuration] independently to each generated value. + * [SequenceStrategy.Unbounded] is lazy and continues until the consumer stops reading it. + * + * @param T Type of values to generate. + * @param sequenceStrategy Controls whether the sequence is bounded or unbounded. + * @param configuration Configuration applied after [someConfig] for every generated value. + * @return A lazy sequence of generated values. + */ + inline fun asSequence( + sequenceStrategy: SequenceStrategy = SequenceStrategy.Unbounded, + noinline configuration: SomeConfigBuilder.() -> Unit = {} + ): Sequence { + return sequence { + val value = { + some { + this.apply(someConfig) + this.apply(configuration) + } + } + + when (sequenceStrategy) { + is SequenceStrategy.Bounded -> { + repeat(times = sequenceStrategy.numberOfElements) { + yield(value()) + } + } + + SequenceStrategy.Unbounded -> { + while (true) { + yield(value()) + } + } + } + } + } + + /** + * Generates one value of type [T] using a one-off configuration. + * + * Unlike [invoke] and [asSequence], this call delegates directly to Some with [configuration]. + * + * @param T Type of value to generate. + * @param configuration Some configuration applied for this value. + * @return A generated value of type [T]. + */ + @JvmOverloads + inline fun create(noinline configuration: SomeConfigBuilder.() -> Unit = {}): T { + return some(configuration) + } + + /** + * Creates another fixture with the supplied configuration. + * + * The returned fixture does not inherit this fixture's configuration. + * + * @param configuration Configuration for the returned fixture. + * @return A new fixture configured with [configuration]. + */ + fun new(configuration: SomeConfigBuilder.() -> Unit = {}): Fixture { + return Fixture(configuration) + } +} + +/** + * Creates a KotlinFixture compatibility generator. + * + * @param configuration Some configuration applied to values generated by the returned fixture. + * @return A configured [Fixture]. + */ +fun kotlinFixture(configuration: SomeConfigBuilder.() -> Unit = {}) = + Fixture(configuration) diff --git a/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/SequenceStrategy.kt b/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/SequenceStrategy.kt new file mode 100644 index 00000000..86884c1b --- /dev/null +++ b/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/SequenceStrategy.kt @@ -0,0 +1,22 @@ +package dev.appoutlet.some.compat.kotlinfixture + +/** + * Strategy controlling the length of sequences generated by [Fixture.asSequence]. + */ +sealed interface SequenceStrategy { + /** + * Generates values until the consumer stops reading the sequence. + */ + data object Unbounded : SequenceStrategy + + /** + * Generates exactly [numberOfElements] values. + * + * @property numberOfElements Number of values to generate. Must be non-negative. + */ + data class Bounded(val numberOfElements: Int) : SequenceStrategy { + init { + require(numberOfElements >= 0) { "numberOfElements must be >= 0" } + } + } +} diff --git a/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/FactoryAndPropertyTest.kt b/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/FactoryAndPropertyTest.kt new file mode 100644 index 00000000..f79b01b9 --- /dev/null +++ b/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/FactoryAndPropertyTest.kt @@ -0,0 +1,52 @@ +package dev.appoutlet.some.compat.kotlinfixture + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class FactoryAndPropertyTest { + + @Test + fun `factory overrides generated values`() { + val fixture = kotlinFixture { + factory(String::class) { "fixed-value" } + factory(GenericBox::class) { GenericBox(777) } + } + + assertEquals("fixed-value", fixture()) + assertEquals(GenericBox(777), fixture>()) + } + + @Test + fun `factory can generate nested values`() { + lateinit var fixture: Fixture + fixture = kotlinFixture { + factory(TestUser::class) { + TestUser( + id = random.nextInt(100, 201), + name = fixture(), + age = 30, + ) + } + factory(String::class) { "nested" } + } + + val user: TestUser = fixture() + + assertTrue(user.id in 100..200) + assertEquals("nested", user.name) + } + + @Test + fun `property factory overrides constructor properties`() { + val fixture = kotlinFixture { + property(TestUser::name) { "overridden" } + property(TestUser::age) { 50 } + } + + val user: TestUser = fixture() + + assertEquals("overridden", user.name) + assertEquals(50, user.age) + } +} diff --git a/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/FixtureTest.kt b/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/FixtureTest.kt new file mode 100644 index 00000000..0d3bead9 --- /dev/null +++ b/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/FixtureTest.kt @@ -0,0 +1,83 @@ +package dev.appoutlet.some.compat.kotlinfixture + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +class FixtureTest { + + @Test + fun `fixture generates primitive and data class values`() { + val fixture = kotlinFixture() + + val number: Int = fixture() + val text: String = fixture() + val user: TestUser = fixture() + + assertNotNull(number) + assertNotNull(text) + assertNotNull(user) + assertNotNull(user.name) + } + + @Test + fun `fixture selects values from a non-empty range`() { + val fixture = kotlinFixture() + val values = listOf("Alice", "Bob", "Charlie") + + val result: String = fixture(values) + + assertTrue(result in values) + } + + @Test + fun `fixture generates a value when the range is empty`() { + val fixture = kotlinFixture() + + val result: String = fixture(emptyList()) + + assertTrue(result.isNotEmpty()) + } + + @Test + fun `per-call configuration overrides the fixture configuration only for that call`() { + val fixture = kotlinFixture { + factory(String::class) { "default" } + } + + val overridden: String = fixture { + factory(String::class) { "override" } + } + + assertEquals("override", overridden) + assertEquals("default", fixture()) + } + + @Test + fun `new creates a fixture using the supplied configuration`() { + val fixture = kotlinFixture { + factory(String::class) { "parent" } + } + + val newFixture = fixture.new { + factory(Int::class) { 42 } + } + + assertEquals(42, newFixture()) + assertEquals("parent", fixture()) + } + + @Test + fun `create generates a value with one-off configuration`() { + val fixture = kotlinFixture { + factory(String::class) { "fixture" } + } + + val result: Int = fixture.create { + factory(Int::class) { 42 } + } + + assertEquals(42, result) + } +} diff --git a/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/SequenceStrategyTest.kt b/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/SequenceStrategyTest.kt new file mode 100644 index 00000000..f642230d --- /dev/null +++ b/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/SequenceStrategyTest.kt @@ -0,0 +1,46 @@ +package dev.appoutlet.some.compat.kotlinfixture + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +class SequenceStrategyTest { + + @Test + fun `unbounded sequence remains lazy`() { + val fixture = kotlinFixture { + factory(Int::class) { 7 } + } + + val values = fixture.asSequence(SequenceStrategy.Unbounded).take(5).toList() + + assertEquals(listOf(7, 7, 7, 7, 7), values) + } + + @Test + fun `bounded sequence emits the requested number of values`() { + val fixture = kotlinFixture { + factory(String::class) { "value" } + } + + val values = fixture.asSequence(SequenceStrategy.Bounded(3)).toList() + + assertEquals(listOf("value", "value", "value"), values) + } + + @Test + fun `bounded sequence with zero elements is empty`() { + val values = kotlinFixture() + .asSequence(SequenceStrategy.Bounded(0)) + .toList() + + assertEquals(emptyList(), values) + } + + @Test + fun `bounded sequence rejects negative element counts`() { + assertFailsWith { + SequenceStrategy.Bounded(-1) + } + } +} diff --git a/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/TestModels.kt b/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/TestModels.kt new file mode 100644 index 00000000..4a994951 --- /dev/null +++ b/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/TestModels.kt @@ -0,0 +1,9 @@ +package dev.appoutlet.some.compat.kotlinfixture + +internal data class TestUser( + val id: Int, + val name: String, + val age: Int = 30 +) + +internal data class GenericBox(val value: T) diff --git a/settings.gradle.kts b/settings.gradle.kts index 3906e1a5..c00dbf05 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -4,6 +4,7 @@ import kotlinx.kover.gradle.plugin.dsl.CoverageUnit pluginManagement { repositories { google() + maven("https://maven-central.storage-download.googleapis.com/maven2/") mavenCentral() maven("https://plugins.gradle.org/m2/") } @@ -12,6 +13,7 @@ pluginManagement { dependencyResolutionManagement { repositories { google() + maven("https://maven-central.storage-download.googleapis.com/maven2/") mavenCentral() } } @@ -29,6 +31,7 @@ include(":core") include(":android") include(":kotest") include(":retrofit") +include(":kotlin-fixture") kover { enableCoverage() diff --git a/zensical.toml b/zensical.toml index b342784c..5d558b65 100644 --- a/zensical.toml +++ b/zensical.toml @@ -11,7 +11,8 @@ nav = [ "getting-started.md", { "Migration" = [ "migration/index.md", - "migration/0.2.1-to-0.2.2.md" + "migration/0.2.1-to-0.2.2.md", + "migration/kotlin-fixture-to-some.md" ] }, "configuration.md", { "Strategies" = [