From ccb911aa8428ff7a768b1d2ca040bbf9c316460b Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 18 Sep 2026 14:00:13 +0000 Subject: [PATCH 01/10] feat: Add Appmattus KotlinFixture compatibility module --- README.md | 6 +- build.gradle.kts | 1 + .../some/config/DefaultValueStrategy.kt | 11 ++ .../dev/appoutlet/some/core/FixtureContext.kt | 11 +- .../appoutlet/some/resolver/ClassResolver.kt | 10 +- .../some/resolver/CustomFactoryResolver.kt | 1 + docs/index.md | 3 +- docs/migration/index.md | 1 + docs/migration/kotlin-fixture-to-some.md | 173 ++++++++++++++++++ kotlin-fixture/build.gradle.kts | 71 +++++++ .../CompatibilityFactoryResolver.kt | 47 +++++ .../CompatibilitySubtypeResolver.kt | 52 ++++++ .../compat/kotlinfixture/Configuration.kt | 65 +++++++ .../kotlinfixture/ConfigurationBuilder.kt | 127 +++++++++++++ .../compat/kotlinfixture/FilterBuilder.kt | 39 ++++ .../some/compat/kotlinfixture/Fixture.kt | 140 ++++++++++++++ .../some/compat/kotlinfixture/Generator.kt | 35 ++++ .../compat/kotlinfixture/KotlinFixture.kt | 11 ++ .../kotlinfixture/NullabilityStrategy.kt | 26 +++ .../compat/kotlinfixture/OptionalStrategy.kt | 26 +++ .../compat/kotlinfixture/RecursionStrategy.kt | 22 +++ .../compat/kotlinfixture/SequenceStrategy.kt | 22 +++ .../kotlinfixture/ConfigurationBuilderTest.kt | 46 +++++ .../compat/kotlinfixture/EdgeCasesTest.kt | 88 +++++++++ .../kotlinfixture/FactoryCompatibilityTest.kt | 59 ++++++ .../kotlinfixture/FilterCompatibilityTest.kt | 36 ++++ .../some/compat/kotlinfixture/FixtureTest.kt | 75 ++++++++ .../MigrationApiCompilationTest.kt | 30 +++ .../NullableStrategyCompatibilityTest.kt | 47 +++++ .../OptionalStrategyCompatibilityTest.kt | 28 +++ .../PropertyCompatibilityTest.kt | 30 +++ .../kotlinfixture/RandomCompatibilityTest.kt | 56 ++++++ .../RecursionStrategyCompatibilityTest.kt | 41 +++++ .../kotlinfixture/SequenceStrategyTest.kt | 57 ++++++ .../kotlinfixture/SubtypeCompatibilityTest.kt | 41 +++++ .../some/compat/kotlinfixture/TestModels.kt | 25 +++ settings.gradle.kts | 3 + zensical.toml | 3 +- 38 files changed, 1559 insertions(+), 6 deletions(-) create mode 100644 docs/migration/kotlin-fixture-to-some.md create mode 100644 kotlin-fixture/build.gradle.kts create mode 100644 kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/CompatibilityFactoryResolver.kt create mode 100644 kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/CompatibilitySubtypeResolver.kt create mode 100644 kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/Configuration.kt create mode 100644 kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/ConfigurationBuilder.kt create mode 100644 kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/FilterBuilder.kt create mode 100644 kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/Fixture.kt create mode 100644 kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/Generator.kt create mode 100644 kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/KotlinFixture.kt create mode 100644 kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/NullabilityStrategy.kt create mode 100644 kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/OptionalStrategy.kt create mode 100644 kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/RecursionStrategy.kt create mode 100644 kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/SequenceStrategy.kt create mode 100644 kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/ConfigurationBuilderTest.kt create mode 100644 kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/EdgeCasesTest.kt create mode 100644 kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/FactoryCompatibilityTest.kt create mode 100644 kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/FilterCompatibilityTest.kt create mode 100644 kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/FixtureTest.kt create mode 100644 kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/MigrationApiCompilationTest.kt create mode 100644 kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/NullableStrategyCompatibilityTest.kt create mode 100644 kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/OptionalStrategyCompatibilityTest.kt create mode 100644 kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/PropertyCompatibilityTest.kt create mode 100644 kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/RandomCompatibilityTest.kt create mode 100644 kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/RecursionStrategyCompatibilityTest.kt create mode 100644 kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/SequenceStrategyTest.kt create mode 100644 kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/SubtypeCompatibilityTest.kt create mode 100644 kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/TestModels.kt diff --git a/README.md b/README.md index 346e716b..7f2587df 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. + 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/DefaultValueStrategy.kt b/core/src/main/kotlin/dev/appoutlet/some/config/DefaultValueStrategy.kt index b2e4fc2f..0e2c9482 100644 --- a/core/src/main/kotlin/dev/appoutlet/some/config/DefaultValueStrategy.kt +++ b/core/src/main/kotlin/dev/appoutlet/some/config/DefaultValueStrategy.kt @@ -38,6 +38,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/core/FixtureContext.kt b/core/src/main/kotlin/dev/appoutlet/some/core/FixtureContext.kt index fd24be6c..feca74cc 100644 --- a/core/src/main/kotlin/dev/appoutlet/some/core/FixtureContext.kt +++ b/core/src/main/kotlin/dev/appoutlet/some/core/FixtureContext.kt @@ -2,6 +2,7 @@ package dev.appoutlet.some.core import kotlin.random.Random import kotlin.reflect.KType +import kotlin.reflect.typeOf /** * Runtime context provided as the receiver for custom factory functions. @@ -35,9 +36,17 @@ import kotlin.reflect.KType * @property random Random source for factory-generated values. This respects the configured seed when one is set. * @property resolutionStack Types currently being resolved, ordered from the outermost request to the current type. * @property strategyProvider Provides access to all registered generation strategies by their base type. + * @property resolver Optional function allowing custom factories to perform nested resolution via the active chain. */ data class FixtureContext( val random: Random, val resolutionStack: List, val strategyProvider: StrategyProvider, -) + val resolver: ((KType) -> Any?)? = null, +) { + /** + * Generates a fixture value of type [T] through the active resolver chain. + */ + inline fun fixture(): T = + (resolver?.invoke(typeOf()) ?: error("Resolver is not available in this context")) as T +} 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..83fc4468 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) @@ -173,6 +178,7 @@ class ClassResolver( random = random, resolutionStack = chain.stack, strategyProvider = strategyProvider, + resolver = chain::resolve, ) return param to propertyFactory(context) diff --git a/core/src/main/kotlin/dev/appoutlet/some/resolver/CustomFactoryResolver.kt b/core/src/main/kotlin/dev/appoutlet/some/resolver/CustomFactoryResolver.kt index 6780fa2c..bf2cf6a2 100644 --- a/core/src/main/kotlin/dev/appoutlet/some/resolver/CustomFactoryResolver.kt +++ b/core/src/main/kotlin/dev/appoutlet/some/resolver/CustomFactoryResolver.kt @@ -60,6 +60,7 @@ class CustomFactoryResolver( random = random, resolutionStack = chain.stack, strategyProvider = strategyProvider, + resolver = chain::resolve, ) return typeFactory.invoke(context) 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..5c819f00 --- /dev/null +++ b/docs/migration/kotlin-fixture-to-some.md @@ -0,0 +1,173 @@ +# Migrating from Appmattus KotlinFixture to Some + +This guide explains how to migrate projects from Appmattus `kotlinfixture` to Some's compatibility module `dev.appoutlet:some-kotlin-fixture`. + +The compatibility module allows you to replace dependencies and imports while keeping existing `kotlinFixture` configuration and call sites intact. + +## 1. Dependency Replacement + +Replace the Appmattus `fixture` dependency with `some-kotlin-fixture`: + +=== "Gradle (Kotlin DSL)" + + ```kotlin + dependencies { + // Remove: + // implementation("com.appmattus.fixture:fixture:1.2.0") + + // Add: + implementation("dev.appoutlet:some-kotlin-fixture:0.4.0") + } + ``` + +=== "Gradle (Groovy DSL)" + + ```groovy + dependencies { + // Remove: + // implementation 'com.appmattus.fixture:fixture:1.2.0' + + // Add: + implementation 'dev.appoutlet:some-kotlin-fixture:0.4.0' + } + ``` + +=== "Maven" + + ```xml + + dev.appoutlet + some-kotlin-fixture + 0.4.0 + + ``` + +## 2. Import Replacement + +Update wildcard imports in your test files: + +```kotlin +// Remove: +import com.appmattus.kotlinfixture.kotlinFixture + +// Replace with: +import dev.appoutlet.some.compat.kotlinfixture.* +``` + +## 3. Supported Features and API Usage + +### Creating a Fixture + +```kotlin +val fixture = kotlinFixture() + +val user: User = fixture() +val age: Int = fixture(18..65) +val name: String = fixture(listOf("Alice", "Bob", "Charlie")) +``` + +### Derived Fixtures (`Fixture.new`) + +Derived fixtures inherit parent configuration without mutating the parent instance: + +```kotlin +val baseFixture = kotlinFixture { + factory { "base" } +} + +val derivedFixture = baseFixture.new { + factory { 42 } +} +``` + +### Sequences (`SequenceStrategy`) + +Generate sequences lazily: + +```kotlin +// Unbounded sequence +val infiniteStream = fixture.asSequence(SequenceStrategy.Unbounded) + +// Bounded sequence +val batch = fixture.asSequence(SequenceStrategy.Bounded(10)).toList() +``` + +### Type Factories + +Override type resolution by exact `KType`: + +```kotlin +val fixture = kotlinFixture { + factory { "fixed-value" } + factory { User(id = range(1..100), name = fixture()) } +} +``` + +### Property Factories + +Override constructor parameter values: + +```kotlin +val fixture = kotlinFixture { + property(User::name) { "OverriddenName" } + property(User::age) { range(18..30) } +} +``` + +### Subtype Mapping + +Map superclasses or interfaces to concrete implementations: + +```kotlin +val fixture = kotlinFixture { + subType() + subType() +} +``` + +### Filters + +Apply predicates and distinct rules to generated values: + +```kotlin +val fixture = kotlinFixture { + filter { + filter { it % 2 == 0 } + } + + filter { + distinct() + } +} +``` + +### Configured Random Source + +Use a deterministic seed for reproducible data: + +```kotlin +val fixture = kotlinFixture { + random = Random(12345L) +} +``` + +### Strategy Aliases + +Configure nullability, default constructor parameters, and recursion behavior: + +```kotlin +val fixture = kotlinFixture { + nullabilityStrategy = NullabilityStrategy.NeverNullStrategy + optionalStrategy = OptionalStrategy.AlwaysOptionalStrategy + recursionStrategy = RecursionStrategy.NullRecursionStrategy +} +``` + +## 4. Known Compatibility Limitations and Deferred Scope + +The following features are currently deferred or unsupported in this phase: + +- **Collection size strategies** (`CollectionStrategy` and `repeatCount`) are deferred to future releases. +- **Appmattus ecosystem plugins** (`fixture-javafaker`, `fixture-generex`, `fixture-kotest`, and Android compatibility modules) are not included in this compatibility module. +- **Deprecated Appmattus `create(...)` APIs** are omitted; use `fixture()` or `fixture(range)` instead. +- **Property factories** support constructor-declared properties handled by Some's `ClassResolver`. Private non-constructor properties or Java setters will throw a clear exception. diff --git a/kotlin-fixture/build.gradle.kts b/kotlin-fixture/build.gradle.kts new file mode 100644 index 00000000..6db34cae --- /dev/null +++ b/kotlin-fixture/build.gradle.kts @@ -0,0 +1,71 @@ +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 { + google() + maven("https://maven-central.storage-download.googleapis.com/maven2/") + mavenCentral() + gradlePluginPortal() +} + +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/CompatibilityFactoryResolver.kt b/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/CompatibilityFactoryResolver.kt new file mode 100644 index 00000000..60b19075 --- /dev/null +++ b/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/CompatibilityFactoryResolver.kt @@ -0,0 +1,47 @@ +package dev.appoutlet.some.compat.kotlinfixture + +import dev.appoutlet.some.core.Resolver +import dev.appoutlet.some.core.ResolverChain +import kotlin.random.Random +import kotlin.reflect.KType + +internal class CompatibilityFactoryResolver( + private val factories: Map.() -> Any?>, + private val random: Random, +) : Resolver { + private val resolvingTypes = ThreadLocal.withInitial { mutableSetOf() } + + override fun canResolve(type: KType): Boolean { + return type in factories && type !in resolvingTypes.get() + } + + override fun resolve(type: KType, chain: ResolverChain): Any? { + val factory = factories[type] ?: return null + val activeSet = resolvingTypes.get() + + val generator = object : Generator { + override val random: Random get() = this@CompatibilityFactoryResolver.random + + override fun resolveType(type: KType): R { + @Suppress("UNCHECKED_CAST") + return chain.resolve(type) as R + } + + override fun resolveRange(iterable: Iterable, type: KType): R { + val list = iterable.toList() + return if (list.isNotEmpty()) { + list[random.nextInt(list.size)] + } else { + resolveType(type) + } + } + } + + activeSet.add(type) + try { + return factory.invoke(generator) + } finally { + activeSet.remove(type) + } + } +} diff --git a/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/CompatibilitySubtypeResolver.kt b/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/CompatibilitySubtypeResolver.kt new file mode 100644 index 00000000..ac1946ee --- /dev/null +++ b/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/CompatibilitySubtypeResolver.kt @@ -0,0 +1,52 @@ +package dev.appoutlet.some.compat.kotlinfixture + +import dev.appoutlet.some.core.Resolver +import dev.appoutlet.some.core.ResolverChain +import kotlin.reflect.KClass +import kotlin.reflect.KType +import kotlin.reflect.full.createType +import kotlin.reflect.full.isSubclassOf + +internal class CompatibilitySubtypeResolver( + private val subTypes: Map, KClass<*>>, +) : Resolver { + private val resolvingSuperTypes = ThreadLocal.withInitial { mutableSetOf>() } + + override fun canResolve(type: KType): Boolean { + val kClass = type.classifier as? KClass<*> ?: return false + return kClass in subTypes && kClass !in resolvingSuperTypes.get() + } + + @Suppress("ReturnCount") + override fun resolve(type: KType, chain: ResolverChain): Any? { + val kClass = type.classifier as? KClass<*> ?: return null + val subClass = subTypes[kClass] ?: return null + val activeSet = resolvingSuperTypes.get() + + require(subClass.isSubclassOf(kClass)) { + "Configured subType ${subClass.qualifiedName} is not a subclass of ${kClass.qualifiedName}" + } + + val targetType = if (subClass.typeParameters.isNotEmpty() && type.arguments.isNotEmpty()) { + subClass.createType( + arguments = type.arguments.take(subClass.typeParameters.size), + nullable = type.isMarkedNullable + ) + } else { + subClass.createType(nullable = type.isMarkedNullable) + } + + activeSet.add(kClass) + try { + val resolved = chain.resolve(targetType) + if (resolved != null && !type.isMarkedNullable) { + check(kClass.isInstance(resolved)) { + "Resolved subtype instance $resolved is not assignable to ${kClass.qualifiedName}" + } + } + return resolved + } finally { + activeSet.remove(kClass) + } + } +} diff --git a/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/Configuration.kt b/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/Configuration.kt new file mode 100644 index 00000000..27ee12cd --- /dev/null +++ b/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/Configuration.kt @@ -0,0 +1,65 @@ +package dev.appoutlet.some.compat.kotlinfixture + +import dev.appoutlet.some.config.DefaultValueStrategy +import dev.appoutlet.some.config.NullableStrategy +import dev.appoutlet.some.config.SomeConfig +import dev.appoutlet.some.core.FixtureContext +import kotlin.random.Random +import kotlin.reflect.KClass +import kotlin.reflect.KType + +/** + * Immutable configuration for compatibility fixture generation. + */ +data class Configuration( + val random: Random = Random.Default, + val typeFactories: Map.() -> Any?> = emptyMap(), + val propertyFactories: Map, String>, Generator.() -> Any?> = emptyMap(), + val subTypes: Map, KClass<*>> = emptyMap(), + val filters: Map> = emptyMap(), + val nullabilityStrategy: NullabilityStrategy = NullabilityStrategy.NeverNullStrategy, + val optionalStrategy: OptionalStrategy = OptionalStrategy.AlwaysOptionalStrategy, + val recursionStrategy: RecursionStrategy = RecursionStrategy.NullRecursionStrategy, +) { + internal fun toSomeConfig(): SomeConfig { + val effectiveNullableStrategy: NullableStrategy = when { + recursionStrategy is RecursionStrategy.NullRecursionStrategy && + nullabilityStrategy is NullabilityStrategy.NeverNullStrategy -> NullableStrategy.NullOnCircularReference + else -> nullabilityStrategy.toSomeStrategy() + } + + val mappedPropertyFactories = propertyFactories.mapValues { (_, generatorLambda) -> + val contextLambda: FixtureContext.() -> Any? = { + val ctx = this + val generator = object : Generator { + override val random: Random get() = ctx.random + + override fun resolveType(type: KType): R { + val res = ctx.resolver ?: error("Resolver not available in FixtureContext") + @Suppress("UNCHECKED_CAST") + return res(type) as R + } + + override fun resolveRange(iterable: Iterable, type: KType): R { + val list = iterable.toList() + return if (list.isNotEmpty()) { + list[random.nextInt(list.size)] + } else { + resolveType(type) + } + } + } + generatorLambda(generator) + } + contextLambda + } + + return SomeConfig( + strategies = mapOf( + NullableStrategy::class to effectiveNullableStrategy, + DefaultValueStrategy::class to optionalStrategy.toSomeStrategy(), + ), + propertyFactories = mappedPropertyFactories, + ) + } +} diff --git a/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/ConfigurationBuilder.kt b/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/ConfigurationBuilder.kt new file mode 100644 index 00000000..c77e0487 --- /dev/null +++ b/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/ConfigurationBuilder.kt @@ -0,0 +1,127 @@ +package dev.appoutlet.some.compat.kotlinfixture + +import kotlin.random.Random +import kotlin.reflect.KCallable +import kotlin.reflect.KClass +import kotlin.reflect.KParameter +import kotlin.reflect.KProperty +import kotlin.reflect.KProperty1 +import kotlin.reflect.KType +import kotlin.reflect.jvm.javaField +import kotlin.reflect.jvm.javaGetter +import kotlin.reflect.typeOf + +/** + * DSL builder for compatibility fixture configuration. + */ +class ConfigurationBuilder internal constructor(initial: Configuration = Configuration()) { + var random: Random = initial.random + var nullabilityStrategy: NullabilityStrategy = initial.nullabilityStrategy + var optionalStrategy: OptionalStrategy = initial.optionalStrategy + var recursionStrategy: RecursionStrategy = initial.recursionStrategy + + @PublishedApi + internal val typeFactories = initial.typeFactories.toMutableMap() + + @PublishedApi + internal val propertyFactories = initial.propertyFactories.toMutableMap() + + @PublishedApi + internal val subTypes = initial.subTypes.toMutableMap() + + @PublishedApi + internal val filters = initial.filters.toMutableMap() + + /** + * Registers a custom type factory for type [T]. + */ + inline fun factory(noinline factory: Generator.() -> T) { + @Suppress("UNCHECKED_CAST") + typeFactories[typeOf()] = factory as Generator.() -> Any? + } + + /** + * Registers a custom type factory for [type]. + */ + fun factory(type: KType, factory: Generator.() -> Any?) { + typeFactories[type] = factory + } + + /** + * Registers a custom property factory for [property]. + */ + fun property(property: KProperty1, factory: Generator.() -> V) { + val (ownerClass, name) = extractOwnerAndName(property) + @Suppress("UNCHECKED_CAST") + propertyFactories[ownerClass to name] = factory as Generator.() -> Any? + } + + /** + * Registers a custom property factory for [property]. + */ + fun property(property: KProperty, factory: Generator.() -> V) { + val (ownerClass, name) = extractOwnerAndName(property) + @Suppress("UNCHECKED_CAST") + propertyFactories[ownerClass to name] = factory as Generator.() -> Any? + } + + /** + * Maps superclass [Super] to subtype [Sub]. + */ + inline fun subType() { + subType(Super::class, Sub::class) + } + + /** + * Maps [superType] to [subType]. + */ + fun subType(superType: KClass<*>, subType: KClass<*>) { + subTypes[superType] = subType + } + + /** + * Configures a filter for type [T]. + */ + inline fun filter(noinline configuration: FilterBuilder.() -> Unit) { + filter(typeOf(), configuration) + } + + /** + * Configures a filter for [type]. + */ + @Suppress("UNCHECKED_CAST") + fun filter(type: KType, configuration: FilterBuilder.() -> Unit) { + val builder = FilterBuilder().apply(configuration) + filters[type] = builder.build() as FilterSpec + } + + internal fun build(): Configuration = Configuration( + random = random, + typeFactories = typeFactories.toMap(), + propertyFactories = propertyFactories.toMap(), + subTypes = subTypes.toMap(), + filters = filters.toMap(), + nullabilityStrategy = nullabilityStrategy, + optionalStrategy = optionalStrategy, + recursionStrategy = recursionStrategy, + ) + + private fun extractOwnerAndName(property: KProperty<*>): Pair, String> { + val propertyName = property.name + + val instanceParamClass = (property as? KCallable<*>)?.parameters + ?.firstOrNull { it.kind == KParameter.Kind.INSTANCE || it.kind == KParameter.Kind.EXTENSION_RECEIVER } + ?.type?.classifier as? KClass<*> + + val ownerClass = instanceParamClass + ?: (property as? KProperty1<*, *>)?.javaGetter?.declaringClass?.kotlin + ?: (property as? KProperty1<*, *>)?.javaField?.declaringClass?.kotlin + + requireNotNull(ownerClass) { + "Could not determine declaring class for property '${property.name}'. " + + "Unsupported private or non-constructor property." + } + + return ownerClass to propertyName + } +} diff --git a/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/FilterBuilder.kt b/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/FilterBuilder.kt new file mode 100644 index 00000000..afddc517 --- /dev/null +++ b/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/FilterBuilder.kt @@ -0,0 +1,39 @@ +package dev.appoutlet.some.compat.kotlinfixture + +/** + * DSL builder for configuring type filters. + */ +class FilterBuilder { + internal val predicates = mutableListOf<(T) -> Boolean>() + internal var isDistinct: Boolean = false + + /** + * Adds a filter predicate that candidates must satisfy. + */ + fun filter(predicate: (T) -> Boolean) { + predicates.add(predicate) + } + + /** + * Specifies that generated values for this type must be distinct. + */ + fun distinct() { + isDistinct = true + } + + internal fun build(): FilterSpec = FilterSpec(predicates.toList(), isDistinct) +} + +/** + * Specification for filtered type generation. + */ +data class FilterSpec( + val predicates: List<(T) -> Boolean>, + val isDistinct: Boolean +) { + fun matchesPredicate(value: T): Boolean = predicates.all { it(value) } + + fun matches(value: T, seen: Set): Boolean { + return matchesPredicate(value) && (!isDistinct || value !in seen) + } +} 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..de9167ab --- /dev/null +++ b/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/Fixture.kt @@ -0,0 +1,140 @@ +package dev.appoutlet.some.compat.kotlinfixture + +import dev.appoutlet.some.Some +import dev.appoutlet.some.config.NullableStrategy +import dev.appoutlet.some.core.ResolverChain +import kotlin.reflect.KType +import kotlin.reflect.typeOf + +private const val MAX_FILTER_ATTEMPTS = 10000 + +/** + * Main compatibility generator instance. + */ +class Fixture internal constructor( + val configuration: Configuration +) { + @PublishedApi + internal val someInstance: Some = buildSomeInstance(configuration) + + /** + * Generates a fixture value of type [T]. + * + * @param range Iterable to select a value from when non-empty. + * @param configuration Optional per-call configuration overrides. + */ + inline operator fun invoke( + range: Iterable = emptyList(), + noinline configuration: (ConfigurationBuilder.() -> Unit)? = null + ): T { + val targetFixture = if (configuration != null) new(configuration) else this + @Suppress("UNCHECKED_CAST") + return targetFixture.generateValue(typeOf(), range) as T + } + + /** + * Generates a sequence of fixture values of type [T]. + * + * @param sequenceStrategy Strategy controlling sequence bounding. + * @param configuration Optional per-call configuration overrides. + */ + inline fun asSequence( + sequenceStrategy: SequenceStrategy = SequenceStrategy.Unbounded, + noinline configuration: (ConfigurationBuilder.() -> Unit)? = null + ): Sequence { + val targetFixture = if (configuration != null) new(configuration) else this + @Suppress("UNCHECKED_CAST") + return targetFixture.generateSequence(typeOf(), sequenceStrategy) as Sequence + } + + /** + * Creates a new [Fixture] deriving from this instance's configuration. + */ + fun new( + configuration: ConfigurationBuilder.() -> Unit = {} + ): Fixture { + val newConfig = ConfigurationBuilder(this.configuration).apply(configuration).build() + return Fixture(newConfig) + } + + @PublishedApi + internal fun generateValue(type: KType, range: Iterable): Any? { + val rangeList = range.toList() + val filterSpec = configuration.filters[type] + + if (rangeList.isNotEmpty()) { + val eligible = if (filterSpec != null) { + rangeList.filter { filterSpec.matchesPredicate(it) } + } else { + rangeList + } + + if (eligible.isNotEmpty()) { + return eligible[configuration.random.nextInt(eligible.size)] + } + } + + return generateSingleRawValue(type, filterSpec) + } + + private fun generateSingleRawValue(type: KType, filterSpec: FilterSpec?): Any? { + if (filterSpec == null) { + return rawResolve(type) + } + + val seen = mutableSetOf() + var attempts = 0 + + while (attempts < MAX_FILTER_ATTEMPTS) { + attempts++ + val candidate = rawResolve(type) + if (filterSpec.matches(candidate, seen)) { + if (filterSpec.isDistinct) seen.add(candidate) + return candidate + } + } + + error("Filter could not be satisfied after $MAX_FILTER_ATTEMPTS attempts for type $type") + } + + @PublishedApi + internal fun generateSequence(type: KType, sequenceStrategy: SequenceStrategy): Sequence { + val filterSpec = configuration.filters[type] + + val rawSeq = kotlin.sequences.generateSequence { rawResolve(type) } + .filter { candidate -> filterSpec?.matchesPredicate(candidate) ?: true } + .let { seq -> + if (filterSpec?.isDistinct == true) seq.distinct() else seq + } + + return when (sequenceStrategy) { + is SequenceStrategy.Unbounded -> rawSeq + is SequenceStrategy.Bounded -> { + if (sequenceStrategy.numberOfElements == 0) { + emptySequence() + } else { + rawSeq.take(sequenceStrategy.numberOfElements) + } + } + } + } + + private fun rawResolve(type: KType): Any? { + val nullableStrategy = someInstance.config[NullableStrategy::class] + val session = ResolverChain(someInstance.resolvers, nullableStrategy) + return session.resolve(type) + } + + companion object { + private fun buildSomeInstance(config: Configuration): Some { + val someConfig = config.toSomeConfig() + val random = config.random + val nativeResolvers = someConfig.buildResolvers(random) + val customFactoryResolver = CompatibilityFactoryResolver(config.typeFactories, random) + val subtypeResolver = CompatibilitySubtypeResolver(config.subTypes) + + val combinedResolvers = listOf(customFactoryResolver, subtypeResolver) + nativeResolvers + return Some(combinedResolvers, random, someConfig) + } + } +} diff --git a/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/Generator.kt b/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/Generator.kt new file mode 100644 index 00000000..c19322f7 --- /dev/null +++ b/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/Generator.kt @@ -0,0 +1,35 @@ +package dev.appoutlet.some.compat.kotlinfixture + +import kotlin.random.Random +import kotlin.reflect.KType +import kotlin.reflect.typeOf + +/** + * Receiver interface for compatibility factory blocks. + */ +interface Generator { + /** + * Shared random source for generating fixture values. + */ + val random: Random + + /** + * Resolves a fixture value for [type] through the active resolver chain. + */ + fun resolveType(type: KType): R + + /** + * Selects a value from [iterable] or falls back to resolving a value for [type] when [iterable] is empty. + */ + fun resolveRange(iterable: Iterable, type: KType): R +} + +/** + * Resolves a fixture value of type [R] through the active resolver chain. + */ +inline fun Generator<*>.fixture(): R = resolveType(typeOf()) + +/** + * Selects a random element from [iterable], falling back to generating a fixture of type [R] if empty. + */ +inline fun Generator<*>.range(iterable: Iterable): R = resolveRange(iterable, typeOf()) diff --git a/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/KotlinFixture.kt b/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/KotlinFixture.kt new file mode 100644 index 00000000..3856f6eb --- /dev/null +++ b/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/KotlinFixture.kt @@ -0,0 +1,11 @@ +package dev.appoutlet.some.compat.kotlinfixture + +/** + * Creates a compatibility [Fixture] generator configured with [configuration]. + */ +fun kotlinFixture( + configuration: ConfigurationBuilder.() -> Unit = {} +): Fixture { + val builder = ConfigurationBuilder().apply(configuration) + return Fixture(builder.build()) +} diff --git a/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/NullabilityStrategy.kt b/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/NullabilityStrategy.kt new file mode 100644 index 00000000..bd471546 --- /dev/null +++ b/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/NullabilityStrategy.kt @@ -0,0 +1,26 @@ +package dev.appoutlet.some.compat.kotlinfixture + +import dev.appoutlet.some.config.NullableStrategy + +/** + * Compatibility nullability strategies matching Appmattus style. + */ +sealed interface NullabilityStrategy { + fun toSomeStrategy(): NullableStrategy + + data object NeverNullStrategy : NullabilityStrategy { + override fun toSomeStrategy(): NullableStrategy = NullableStrategy.NeverNull + } + + data object AlwaysNullStrategy : NullabilityStrategy { + override fun toSomeStrategy(): NullableStrategy = NullableStrategy.AlwaysNull + } + + data class RandomlyNullStrategy(val nullability: Float = 0.5f) : NullabilityStrategy { + init { + require(nullability in 0f..1f) { "nullability must be between 0.0 and 1.0" } + } + + override fun toSomeStrategy(): NullableStrategy = NullableStrategy.Random(nullability.toDouble()) + } +} diff --git a/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/OptionalStrategy.kt b/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/OptionalStrategy.kt new file mode 100644 index 00000000..b9fd150c --- /dev/null +++ b/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/OptionalStrategy.kt @@ -0,0 +1,26 @@ +package dev.appoutlet.some.compat.kotlinfixture + +import dev.appoutlet.some.config.DefaultValueStrategy + +/** + * Compatibility optional value strategies matching Appmattus style. + */ +sealed interface OptionalStrategy { + fun toSomeStrategy(): DefaultValueStrategy + + data object AlwaysOptionalStrategy : OptionalStrategy { + override fun toSomeStrategy(): DefaultValueStrategy = DefaultValueStrategy.UseDefault + } + + data object NeverOptionalStrategy : OptionalStrategy { + override fun toSomeStrategy(): DefaultValueStrategy = DefaultValueStrategy.Generate + } + + data class RandomlyOptionalStrategy(val optionality: Float = 0.5f) : OptionalStrategy { + init { + require(optionality in 0f..1f) { "optionality must be between 0.0 and 1.0" } + } + + override fun toSomeStrategy(): DefaultValueStrategy = DefaultValueStrategy.Random(optionality) + } +} diff --git a/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/RecursionStrategy.kt b/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/RecursionStrategy.kt new file mode 100644 index 00000000..d6c1fd6c --- /dev/null +++ b/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/RecursionStrategy.kt @@ -0,0 +1,22 @@ +package dev.appoutlet.some.compat.kotlinfixture + +import dev.appoutlet.some.config.NullableStrategy + +/** + * Compatibility recursion strategies matching Appmattus style. + */ +sealed interface RecursionStrategy { + fun toSomeStrategy(): NullableStrategy + + data object NullRecursionStrategy : RecursionStrategy { + override fun toSomeStrategy(): NullableStrategy = NullableStrategy.NullOnCircularReference + } + + data object ThrowingRecursionStrategy : RecursionStrategy { + override fun toSomeStrategy(): NullableStrategy = NullableStrategy.NeverNull + } + + data object UnresolvedRecursionStrategy : RecursionStrategy { + override fun toSomeStrategy(): NullableStrategy = NullableStrategy.NeverNull + } +} 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..8223fb0f --- /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 sequence generation length for [Fixture.asSequence]. + */ +sealed interface SequenceStrategy { + /** + * Generates an unbounded sequence of fixture values. + */ + data object Unbounded : SequenceStrategy + + /** + * Generates a sequence bounded to [numberOfElements]. + * + * @property numberOfElements Exact number of elements the sequence should produce. Must be >= 0. + */ + 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/ConfigurationBuilderTest.kt b/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/ConfigurationBuilderTest.kt new file mode 100644 index 00000000..13a8208b --- /dev/null +++ b/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/ConfigurationBuilderTest.kt @@ -0,0 +1,46 @@ +package dev.appoutlet.some.compat.kotlinfixture + +import kotlin.random.Random +import kotlin.reflect.typeOf +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +class ConfigurationBuilderTest { + + @Test + fun `ConfigurationBuilder initializes default values`() { + val builder = ConfigurationBuilder() + val config = builder.build() + + assertEquals(NullabilityStrategy.NeverNullStrategy, config.nullabilityStrategy) + assertEquals(OptionalStrategy.AlwaysOptionalStrategy, config.optionalStrategy) + assertEquals(RecursionStrategy.NullRecursionStrategy, config.recursionStrategy) + } + + @Test + fun `ConfigurationBuilder stores registrations`() { + val customRandom = Random(42) + val builder = ConfigurationBuilder().apply { + random = customRandom + nullabilityStrategy = NullabilityStrategy.AlwaysNullStrategy + optionalStrategy = OptionalStrategy.NeverOptionalStrategy + recursionStrategy = RecursionStrategy.ThrowingRecursionStrategy + factory { 100 } + property(TestUser::name) { "John" } + subType() + filter { distinct() } + } + + val config = builder.build() + + assertEquals(customRandom, config.random) + assertEquals(NullabilityStrategy.AlwaysNullStrategy, config.nullabilityStrategy) + assertEquals(OptionalStrategy.NeverOptionalStrategy, config.optionalStrategy) + assertEquals(RecursionStrategy.ThrowingRecursionStrategy, config.recursionStrategy) + assertNotNull(config.typeFactories[typeOf()]) + assertNotNull(config.propertyFactories[TestUser::class to "name"]) + assertEquals(Dog::class, config.subTypes[Animal::class]) + assertNotNull(config.filters[typeOf()]) + } +} diff --git a/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/EdgeCasesTest.kt b/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/EdgeCasesTest.kt new file mode 100644 index 00000000..e2a5d3da --- /dev/null +++ b/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/EdgeCasesTest.kt @@ -0,0 +1,88 @@ +package dev.appoutlet.some.compat.kotlinfixture + +import kotlin.reflect.typeOf +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +class EdgeCasesTest { + + @Test + fun `strategy parameter validation throws on out of range values`() { + assertFailsWith { + NullabilityStrategy.RandomlyNullStrategy(-0.1f) + } + assertFailsWith { + NullabilityStrategy.RandomlyNullStrategy(1.1f) + } + assertFailsWith { + OptionalStrategy.RandomlyOptionalStrategy(-0.1f) + } + assertFailsWith { + OptionalStrategy.RandomlyOptionalStrategy(1.1f) + } + } + + @Test + fun `RandomlyOptionalStrategy generates values`() { + val fixture = kotlinFixture { + optionalStrategy = OptionalStrategy.RandomlyOptionalStrategy(0.5f) + } + val user: TestUser = fixture() + assertNotNull(user) + } + + @Test + fun `ConfigurationBuilder untyped methods store registrations`() { + val builder = ConfigurationBuilder().apply { + factory(typeOf()) { "untyped-factory" } + subType(Animal::class, Dog::class) + filter(typeOf()) { + filter { it > 0 } + } + } + val config = builder.build() + val fixture = Fixture(config) + + assertEquals("untyped-factory", fixture()) + assertTrue(fixture() is Dog) + assertTrue(fixture() > 0) + } + + @Test + fun `un-satisfiable filter throws IllegalStateException after max attempts`() { + val fixture = kotlinFixture { + filter { + filter { false } + } + } + + assertFailsWith { + fixture() + } + } + + @Test + fun `range selection with filter filters items before picking`() { + val fixture = kotlinFixture { + filter { + filter { it % 2 == 0 } + } + } + + val selected: Int = fixture(listOf(1, 3, 4, 5)) + assertEquals(4, selected) + } + + @Test + fun `generic subtype resolution maps type arguments`() { + val fixture = kotlinFixture { + subType(List::class, ArrayList::class) + } + + val list: List = fixture() + assertNotNull(list) + } +} diff --git a/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/FactoryCompatibilityTest.kt b/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/FactoryCompatibilityTest.kt new file mode 100644 index 00000000..2970e917 --- /dev/null +++ b/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/FactoryCompatibilityTest.kt @@ -0,0 +1,59 @@ +package dev.appoutlet.some.compat.kotlinfixture + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class FactoryCompatibilityTest { + + @Test + fun `type factory resolves exact generic and nullable types`() { + val fixture = kotlinFixture { + factory { "custom-string" } + factory { null } + factory> { GenericBox(777) } + } + + assertEquals("custom-string", fixture()) + assertNull(fixture()) + assertEquals(GenericBox(777), fixture>()) + } + + @Test + fun `factory supports nested fixture and range calls`() { + val fixture = kotlinFixture { + factory { + TestUser( + id = range(100..200), + name = fixture(), + age = range(listOf(20, 25, 30)) + ) + } + } + + val user: TestUser = fixture() + assertTrue(user.id in 100..200) + assertNotNull(user.name) + assertTrue(user.age in listOf(20, 25, 30)) + } + + @Test + fun `factory avoids recursive self-invocation when resolving nested different types`() { + val fixture = kotlinFixture { + factory { "hello" } + factory { + TestUser( + id = 42, + name = fixture(), + age = 30 + ) + } + } + + val user: TestUser = fixture() + assertEquals(42, user.id) + assertEquals("hello", user.name) + } +} diff --git a/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/FilterCompatibilityTest.kt b/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/FilterCompatibilityTest.kt new file mode 100644 index 00000000..8365b511 --- /dev/null +++ b/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/FilterCompatibilityTest.kt @@ -0,0 +1,36 @@ +package dev.appoutlet.some.compat.kotlinfixture + +import kotlin.test.Test +import kotlin.test.assertEquals + +class FilterCompatibilityTest { + + @Test + fun `filter predicate filters direct fixture generation`() { + val fixture = kotlinFixture { + filter { + filter { it % 2 == 0 } + } + } + + repeat(10) { + val val1: Int = fixture() + assertEquals(0, val1 % 2) + } + } + + @Test + fun `distinct filter generates unique values in sequence`() { + val fixture = kotlinFixture { + factory { range(listOf(1, 1, 2, 2, 3, 3)) } + filter { + distinct() + } + } + + val sequence = fixture.asSequence(SequenceStrategy.Bounded(3)).toList() + + assertEquals(3, sequence.size) + assertEquals(sequence.size, sequence.toSet().size) + } +} 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..4ae850c9 --- /dev/null +++ b/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/FixtureTest.kt @@ -0,0 +1,75 @@ +package dev.appoutlet.some.compat.kotlinfixture + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +class FixtureTest { + + @Test + fun `fixture generates primitives and data classes`() { + val fixture = kotlinFixture() + + val intVal: Int = fixture() + val stringVal: String = fixture() + val userVal: TestUser = fixture() + + assertNotNull(intVal) + assertNotNull(stringVal) + assertNotNull(userVal) + assertNotNull(userVal.name) + } + + @Test + fun `fixture selects value from non-empty iterable`() { + val fixture = kotlinFixture() + + val item: String = fixture(listOf("A", "B", "C")) + assertTrue(item in listOf("A", "B", "C")) + + val number: Int = fixture(10..20) + assertTrue(number in 10..20) + } + + @Test + fun `fixture falls back to generated value for empty iterable`() { + val fixture = kotlinFixture() + + val item: String = fixture(emptyList()) + assertNotNull(item) + assertTrue(item.isNotBlank()) + } + + @Test + fun `fixture invoke with per-call configuration does not mutate parent`() { + val parent = kotlinFixture() + + val overridden: Int = parent { + factory { 999 } + } + assertEquals(999, overridden) + + val original: Int = parent() + assertNotEquals(999, original) + } + + @Test + fun `new creates derived fixture preserving configuration`() { + val base = kotlinFixture { + factory { "base-string" } + } + + val derived = base.new { + factory { 123 } + } + + assertEquals("base-string", derived()) + assertEquals(123, derived()) + + // Base remains unchanged for Int + assertEquals("base-string", base()) + assertNotEquals(123, base()) + } +} diff --git a/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/MigrationApiCompilationTest.kt b/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/MigrationApiCompilationTest.kt new file mode 100644 index 00000000..fc2b75ee --- /dev/null +++ b/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/MigrationApiCompilationTest.kt @@ -0,0 +1,30 @@ +package dev.appoutlet.some.compat.kotlinfixture + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +class MigrationApiCompilationTest { + + @Test + fun `migrated code using wildcard import compiles and executes correctly`() { + val fixture: Fixture = kotlinFixture { + nullabilityStrategy = NullabilityStrategy.NeverNullStrategy + optionalStrategy = OptionalStrategy.AlwaysOptionalStrategy + recursionStrategy = RecursionStrategy.NullRecursionStrategy + + factory { "migrated" } + property(TestUser::id) { range(1..100) } + subType() + filter { filter { it > 0 } } + } + + val user: TestUser = fixture() + val animal: Animal = fixture() + val sequence: List = fixture.asSequence(SequenceStrategy.Bounded(3)).toList() + + assertEquals("migrated", user.name) + assertNotNull(animal) + assertEquals(3, sequence.size) + } +} diff --git a/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/NullableStrategyCompatibilityTest.kt b/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/NullableStrategyCompatibilityTest.kt new file mode 100644 index 00000000..de88ffc0 --- /dev/null +++ b/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/NullableStrategyCompatibilityTest.kt @@ -0,0 +1,47 @@ +package dev.appoutlet.some.compat.kotlinfixture + +import kotlin.test.Test +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class NullableStrategyCompatibilityTest { + + @Test + fun `NeverNullStrategy produces non-null values for nullable types`() { + val fixture = kotlinFixture { + nullabilityStrategy = NullabilityStrategy.NeverNullStrategy + } + + repeat(20) { + val value: String? = fixture() + assertNotNull(value) + } + } + + @Test + fun `AlwaysNullStrategy produces null values for nullable types`() { + val fixture = kotlinFixture { + nullabilityStrategy = NullabilityStrategy.AlwaysNullStrategy + } + + repeat(20) { + val value: String? = fixture() + assertNull(value) + } + } + + @Test + fun `RandomlyNullStrategy produces mix of null and non-null values`() { + val fixture = kotlinFixture { + nullabilityStrategy = NullabilityStrategy.RandomlyNullStrategy(0.5f) + } + + val results = List(50) { fixture() } + val nullCount = results.count { it == null } + val nonNullCount = results.count { it != null } + + assertTrue(nullCount > 0, "Expected at least one null value") + assertTrue(nonNullCount > 0, "Expected at least one non-null value") + } +} diff --git a/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/OptionalStrategyCompatibilityTest.kt b/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/OptionalStrategyCompatibilityTest.kt new file mode 100644 index 00000000..dd7ec06f --- /dev/null +++ b/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/OptionalStrategyCompatibilityTest.kt @@ -0,0 +1,28 @@ +package dev.appoutlet.some.compat.kotlinfixture + +import kotlin.test.Test +import kotlin.test.assertEquals + +class OptionalStrategyCompatibilityTest { + + @Test + fun `AlwaysOptionalStrategy uses default constructor values`() { + val fixture = kotlinFixture { + optionalStrategy = OptionalStrategy.AlwaysOptionalStrategy + } + + val user: TestUser = fixture() + assertEquals(30, user.age) + } + + @Test + fun `NeverOptionalStrategy generates values for optional parameters`() { + val fixture = kotlinFixture { + optionalStrategy = OptionalStrategy.NeverOptionalStrategy + factory { 999 } + } + + val user: TestUser = fixture() + assertEquals(999, user.age) + } +} diff --git a/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/PropertyCompatibilityTest.kt b/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/PropertyCompatibilityTest.kt new file mode 100644 index 00000000..b2772d7e --- /dev/null +++ b/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/PropertyCompatibilityTest.kt @@ -0,0 +1,30 @@ +package dev.appoutlet.some.compat.kotlinfixture + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class PropertyCompatibilityTest { + + @Test + fun `property factory overrides parameter value`() { + val fixture = kotlinFixture { + property(TestUser::name) { "OverriddenName" } + property(TestUser::age) { range(50..60) } + } + + val user: TestUser = fixture() + assertEquals("OverriddenName", user.name) + assertTrue(user.age in 50..60) + } + + @Test + fun `property factory supports nested fixture call`() { + val fixture = kotlinFixture { + property(TestUser::name) { fixture().uppercase() } + } + + val user: TestUser = fixture() + assertEquals(user.name, user.name.uppercase()) + } +} diff --git a/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/RandomCompatibilityTest.kt b/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/RandomCompatibilityTest.kt new file mode 100644 index 00000000..3669960f --- /dev/null +++ b/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/RandomCompatibilityTest.kt @@ -0,0 +1,56 @@ +package dev.appoutlet.some.compat.kotlinfixture + +import kotlin.random.Random +import kotlin.test.Test +import kotlin.test.assertEquals + +class RandomCompatibilityTest { + + @Test + fun `deterministic seed produces identical fixture sequence`() { + val seed = 12345L + + val fixture1 = kotlinFixture { + random = Random(seed) + } + + val fixture2 = kotlinFixture { + random = Random(seed) + } + + val list1 = List(5) { fixture1() } + val list2 = List(5) { fixture2() } + + assertEquals(list1, list2) + } + + @Test + fun `configured random is preserved in range and factories`() { + val seed = 9999L + + val fixture1 = kotlinFixture { + random = Random(seed) + factory { + TestUser( + id = range(1..1000), + name = fixture() + ) + } + } + + val fixture2 = kotlinFixture { + random = Random(seed) + factory { + TestUser( + id = range(1..1000), + name = fixture() + ) + } + } + + val user1: TestUser = fixture1() + val user2: TestUser = fixture2() + + assertEquals(user1, user2) + } +} diff --git a/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/RecursionStrategyCompatibilityTest.kt b/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/RecursionStrategyCompatibilityTest.kt new file mode 100644 index 00000000..a91e6e19 --- /dev/null +++ b/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/RecursionStrategyCompatibilityTest.kt @@ -0,0 +1,41 @@ +package dev.appoutlet.some.compat.kotlinfixture + +import dev.appoutlet.some.exception.SomeCircularReferenceException +import kotlin.test.Test +import kotlin.test.assertFailsWith +import kotlin.test.assertNull + +class RecursionStrategyCompatibilityTest { + + @Test + fun `NullRecursionStrategy resolves circular reference as null for nullable field`() { + val fixture = kotlinFixture { + recursionStrategy = RecursionStrategy.NullRecursionStrategy + } + + val node: RecursiveNode = fixture() + assertNull(node.next) + } + + @Test + fun `ThrowingRecursionStrategy throws exception on circular reference`() { + val fixture = kotlinFixture { + recursionStrategy = RecursionStrategy.ThrowingRecursionStrategy + } + + assertFailsWith { + fixture() + } + } + + @Test + fun `UnresolvedRecursionStrategy throws exception on circular reference`() { + val fixture = kotlinFixture { + recursionStrategy = RecursionStrategy.UnresolvedRecursionStrategy + } + + assertFailsWith { + fixture() + } + } +} 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..59506d7b --- /dev/null +++ b/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/SequenceStrategyTest.kt @@ -0,0 +1,57 @@ +package dev.appoutlet.some.compat.kotlinfixture + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertTrue + +class SequenceStrategyTest { + + @Test + fun `Unbounded strategy creates infinite sequence`() { + val fixture = kotlinFixture() + + val items = fixture.asSequence(SequenceStrategy.Unbounded).take(10).toList() + + assertEquals(10, items.size) + } + + @Test + fun `Bounded 0 produces empty sequence`() { + val fixture = kotlinFixture() + + val items = fixture.asSequence(SequenceStrategy.Bounded(0)).toList() + + assertTrue(items.isEmpty()) + } + + @Test + fun `Bounded N produces exactly N elements`() { + val fixture = kotlinFixture() + + val items = fixture.asSequence(SequenceStrategy.Bounded(5)).toList() + + assertEquals(5, items.size) + } + + @Test + fun `Bounded throws for negative element count`() { + assertFailsWith { + SequenceStrategy.Bounded(-1) + } + } + + @Test + fun `filters are applied before bounded limit`() { + val fixture = kotlinFixture { + filter { + filter { it % 2 == 0 } + } + } + + val items = fixture.asSequence(SequenceStrategy.Bounded(5)).toList() + + assertEquals(5, items.size) + assertTrue(items.all { it % 2 == 0 }) + } +} diff --git a/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/SubtypeCompatibilityTest.kt b/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/SubtypeCompatibilityTest.kt new file mode 100644 index 00000000..81ac6cc6 --- /dev/null +++ b/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/SubtypeCompatibilityTest.kt @@ -0,0 +1,41 @@ +package dev.appoutlet.some.compat.kotlinfixture + +import kotlin.test.Test +import kotlin.test.assertIs + +class SubtypeCompatibilityTest { + + @Test + fun `subType maps superclass or interface to concrete subtype`() { + val fixture = kotlinFixture { + subType() + } + + val animal: Animal = fixture() + assertIs(animal) + } + + @Test + fun `subType preserves derived configuration overrides`() { + val base = kotlinFixture { + subType() + } + + val derived = base.new { + subType() + } + + assertIs(base()) + assertIs(derived()) + } + + @Test + fun `subType mapping Number to Int generates Int instance`() { + val fixture = kotlinFixture { + subType() + } + + val number: Number = fixture() + assertIs(number) + } +} 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..12890127 --- /dev/null +++ b/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/TestModels.kt @@ -0,0 +1,25 @@ +package dev.appoutlet.some.compat.kotlinfixture + +internal data class TestUser( + val id: Int, + val name: String, + val age: Int = 30 +) + +internal interface Animal +internal data class Dog(val name: String = "Buddy") : Animal +internal data class Cat(val name: String = "Whiskers") : Animal + +internal data class GenericBox( + val value: T +) + +internal data class RecursiveNode( + val value: Int, + val next: RecursiveNode? +) + +internal data class NonNullableRecursiveNode( + val value: Int, + val next: NonNullableRecursiveNode +) 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" = [ From c29f270cc96e2d114a9dd0eaef629eeaf8c03e31 Mon Sep 17 00:00:00 2001 From: Messias Junior Date: Fri, 18 Sep 2026 15:12:47 +0100 Subject: [PATCH 02/10] revert changes on core --- .../kotlin/dev/appoutlet/some/core/FixtureContext.kt | 11 +---------- .../dev/appoutlet/some/resolver/ClassResolver.kt | 1 - .../appoutlet/some/resolver/CustomFactoryResolver.kt | 1 - 3 files changed, 1 insertion(+), 12 deletions(-) diff --git a/core/src/main/kotlin/dev/appoutlet/some/core/FixtureContext.kt b/core/src/main/kotlin/dev/appoutlet/some/core/FixtureContext.kt index feca74cc..fd24be6c 100644 --- a/core/src/main/kotlin/dev/appoutlet/some/core/FixtureContext.kt +++ b/core/src/main/kotlin/dev/appoutlet/some/core/FixtureContext.kt @@ -2,7 +2,6 @@ package dev.appoutlet.some.core import kotlin.random.Random import kotlin.reflect.KType -import kotlin.reflect.typeOf /** * Runtime context provided as the receiver for custom factory functions. @@ -36,17 +35,9 @@ import kotlin.reflect.typeOf * @property random Random source for factory-generated values. This respects the configured seed when one is set. * @property resolutionStack Types currently being resolved, ordered from the outermost request to the current type. * @property strategyProvider Provides access to all registered generation strategies by their base type. - * @property resolver Optional function allowing custom factories to perform nested resolution via the active chain. */ data class FixtureContext( val random: Random, val resolutionStack: List, val strategyProvider: StrategyProvider, - val resolver: ((KType) -> Any?)? = null, -) { - /** - * Generates a fixture value of type [T] through the active resolver chain. - */ - inline fun fixture(): T = - (resolver?.invoke(typeOf()) ?: error("Resolver is not available in this context")) as T -} +) 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 83fc4468..4c4f1b60 100644 --- a/core/src/main/kotlin/dev/appoutlet/some/resolver/ClassResolver.kt +++ b/core/src/main/kotlin/dev/appoutlet/some/resolver/ClassResolver.kt @@ -178,7 +178,6 @@ class ClassResolver( random = random, resolutionStack = chain.stack, strategyProvider = strategyProvider, - resolver = chain::resolve, ) return param to propertyFactory(context) diff --git a/core/src/main/kotlin/dev/appoutlet/some/resolver/CustomFactoryResolver.kt b/core/src/main/kotlin/dev/appoutlet/some/resolver/CustomFactoryResolver.kt index bf2cf6a2..6780fa2c 100644 --- a/core/src/main/kotlin/dev/appoutlet/some/resolver/CustomFactoryResolver.kt +++ b/core/src/main/kotlin/dev/appoutlet/some/resolver/CustomFactoryResolver.kt @@ -60,7 +60,6 @@ class CustomFactoryResolver( random = random, resolutionStack = chain.stack, strategyProvider = strategyProvider, - resolver = chain::resolve, ) return typeFactory.invoke(context) From 30b67aed36345e4ce7f01e690a0f605f96136cf3 Mon Sep 17 00:00:00 2001 From: Messias Junior Date: Fri, 18 Sep 2026 15:24:06 +0100 Subject: [PATCH 03/10] DefaultValueStrategy.Random test --- .../DefaultValueStrategyIntegrationTest.kt | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) 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 { From 04c3a38702abe1335bde5a5c2aa45823b212a740 Mon Sep 17 00:00:00 2001 From: Messias Junior Date: Fri, 18 Sep 2026 15:25:23 +0100 Subject: [PATCH 04/10] update docs --- README.md | 2 +- docs/migration/kotlin-fixture-to-some.md | 13 ++++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 7f2587df..54882dc8 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ dependencies { // Retrofit must be declared directly by the consuming project. testImplementation("dev.appoutlet:some-retrofit:{version}") - // Optional: Appmattus KotlinFixture compatibility. + // Optional: Appmattus KotlinFixture compatibility. For better migration testImplementation("dev.appoutlet:some-kotlin-fixture:{version}") } ``` diff --git a/docs/migration/kotlin-fixture-to-some.md b/docs/migration/kotlin-fixture-to-some.md index 5c819f00..2bedc34f 100644 --- a/docs/migration/kotlin-fixture-to-some.md +++ b/docs/migration/kotlin-fixture-to-some.md @@ -44,14 +44,21 @@ Replace the Appmattus `fixture` dependency with `some-kotlin-fixture`: ## 2. Import Replacement -Update wildcard imports in your test files: +Replace the Appmattus imports with explicit imports for the compatibility symbols you actually use: ```kotlin // Remove: import com.appmattus.kotlinfixture.kotlinFixture -// Replace with: -import dev.appoutlet.some.compat.kotlinfixture.* +// Add (import only what you use): +import dev.appoutlet.some.compat.kotlinfixture.Fixture +import dev.appoutlet.some.compat.kotlinfixture.NullabilityStrategy +import dev.appoutlet.some.compat.kotlinfixture.OptionalStrategy +import dev.appoutlet.some.compat.kotlinfixture.RecursionStrategy +import dev.appoutlet.some.compat.kotlinfixture.SequenceStrategy +import dev.appoutlet.some.compat.kotlinfixture.fixture +import dev.appoutlet.some.compat.kotlinfixture.kotlinFixture +import dev.appoutlet.some.compat.kotlinfixture.range ``` ## 3. Supported Features and API Usage From b2a774507eccb058c13057a8f1eeab0574c1297a Mon Sep 17 00:00:00 2001 From: Messias Junior Date: Fri, 18 Sep 2026 15:27:38 +0100 Subject: [PATCH 05/10] fix gradle --- kotlin-fixture/build.gradle.kts | 3 --- 1 file changed, 3 deletions(-) diff --git a/kotlin-fixture/build.gradle.kts b/kotlin-fixture/build.gradle.kts index 6db34cae..45d3fcad 100644 --- a/kotlin-fixture/build.gradle.kts +++ b/kotlin-fixture/build.gradle.kts @@ -9,10 +9,7 @@ group = rootProject.group version = rootProject.version repositories { - google() - maven("https://maven-central.storage-download.googleapis.com/maven2/") mavenCentral() - gradlePluginPortal() } dependencies { From 02b88df940d3903ef0ac201c95062060f26264b0 Mon Sep 17 00:00:00 2001 From: Messias Junior Date: Fri, 18 Sep 2026 16:14:51 +0100 Subject: [PATCH 06/10] rewrite Configuration.kt --- .../some/config/CollectionStrategy.kt | 4 +- .../CompatibilityFactoryResolver.kt | 47 -------------- .../CompatibilitySubtypeResolver.kt | 52 ---------------- .../compat/kotlinfixture/Configuration.kt | 62 ++++--------------- .../some/compat/kotlinfixture/Generator.kt | 30 ++------- 5 files changed, 19 insertions(+), 176 deletions(-) delete mode 100644 kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/CompatibilityFactoryResolver.kt delete mode 100644 kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/CompatibilitySubtypeResolver.kt 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..98b5da5c 100644 --- a/core/src/main/kotlin/dev/appoutlet/some/config/CollectionStrategy.kt +++ b/core/src/main/kotlin/dev/appoutlet/some/config/CollectionStrategy.kt @@ -11,9 +11,11 @@ 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" } } + constructor(size: Int): this(size..size) + companion object { /** * The default collection strategy. diff --git a/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/CompatibilityFactoryResolver.kt b/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/CompatibilityFactoryResolver.kt deleted file mode 100644 index 60b19075..00000000 --- a/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/CompatibilityFactoryResolver.kt +++ /dev/null @@ -1,47 +0,0 @@ -package dev.appoutlet.some.compat.kotlinfixture - -import dev.appoutlet.some.core.Resolver -import dev.appoutlet.some.core.ResolverChain -import kotlin.random.Random -import kotlin.reflect.KType - -internal class CompatibilityFactoryResolver( - private val factories: Map.() -> Any?>, - private val random: Random, -) : Resolver { - private val resolvingTypes = ThreadLocal.withInitial { mutableSetOf() } - - override fun canResolve(type: KType): Boolean { - return type in factories && type !in resolvingTypes.get() - } - - override fun resolve(type: KType, chain: ResolverChain): Any? { - val factory = factories[type] ?: return null - val activeSet = resolvingTypes.get() - - val generator = object : Generator { - override val random: Random get() = this@CompatibilityFactoryResolver.random - - override fun resolveType(type: KType): R { - @Suppress("UNCHECKED_CAST") - return chain.resolve(type) as R - } - - override fun resolveRange(iterable: Iterable, type: KType): R { - val list = iterable.toList() - return if (list.isNotEmpty()) { - list[random.nextInt(list.size)] - } else { - resolveType(type) - } - } - } - - activeSet.add(type) - try { - return factory.invoke(generator) - } finally { - activeSet.remove(type) - } - } -} diff --git a/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/CompatibilitySubtypeResolver.kt b/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/CompatibilitySubtypeResolver.kt deleted file mode 100644 index ac1946ee..00000000 --- a/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/CompatibilitySubtypeResolver.kt +++ /dev/null @@ -1,52 +0,0 @@ -package dev.appoutlet.some.compat.kotlinfixture - -import dev.appoutlet.some.core.Resolver -import dev.appoutlet.some.core.ResolverChain -import kotlin.reflect.KClass -import kotlin.reflect.KType -import kotlin.reflect.full.createType -import kotlin.reflect.full.isSubclassOf - -internal class CompatibilitySubtypeResolver( - private val subTypes: Map, KClass<*>>, -) : Resolver { - private val resolvingSuperTypes = ThreadLocal.withInitial { mutableSetOf>() } - - override fun canResolve(type: KType): Boolean { - val kClass = type.classifier as? KClass<*> ?: return false - return kClass in subTypes && kClass !in resolvingSuperTypes.get() - } - - @Suppress("ReturnCount") - override fun resolve(type: KType, chain: ResolverChain): Any? { - val kClass = type.classifier as? KClass<*> ?: return null - val subClass = subTypes[kClass] ?: return null - val activeSet = resolvingSuperTypes.get() - - require(subClass.isSubclassOf(kClass)) { - "Configured subType ${subClass.qualifiedName} is not a subclass of ${kClass.qualifiedName}" - } - - val targetType = if (subClass.typeParameters.isNotEmpty() && type.arguments.isNotEmpty()) { - subClass.createType( - arguments = type.arguments.take(subClass.typeParameters.size), - nullable = type.isMarkedNullable - ) - } else { - subClass.createType(nullable = type.isMarkedNullable) - } - - activeSet.add(kClass) - try { - val resolved = chain.resolve(targetType) - if (resolved != null && !type.isMarkedNullable) { - check(kClass.isInstance(resolved)) { - "Resolved subtype instance $resolved is not assignable to ${kClass.qualifiedName}" - } - } - return resolved - } finally { - activeSet.remove(kClass) - } - } -} diff --git a/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/Configuration.kt b/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/Configuration.kt index 27ee12cd..ad7e6ad6 100644 --- a/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/Configuration.kt +++ b/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/Configuration.kt @@ -1,9 +1,7 @@ package dev.appoutlet.some.compat.kotlinfixture -import dev.appoutlet.some.config.DefaultValueStrategy -import dev.appoutlet.some.config.NullableStrategy -import dev.appoutlet.some.config.SomeConfig -import dev.appoutlet.some.core.FixtureContext +import dev.appoutlet.some.core.Resolver +import dev.appoutlet.some.defaultResolvers import kotlin.random.Random import kotlin.reflect.KClass import kotlin.reflect.KType @@ -12,54 +10,16 @@ import kotlin.reflect.KType * Immutable configuration for compatibility fixture generation. */ data class Configuration( - val random: Random = Random.Default, - val typeFactories: Map.() -> Any?> = emptyMap(), - val propertyFactories: Map, String>, Generator.() -> Any?> = emptyMap(), + val repeatCount: () -> Int = defaultRepeatCount, + val propertiesRepeatCount: Map, Map Int>> = emptyMap, Map Int>>(), + val properties: Map, Map> = emptyMap(), + val factories: Map = emptyMap(), val subTypes: Map, KClass<*>> = emptyMap(), - val filters: Map> = emptyMap(), - val nullabilityStrategy: NullabilityStrategy = NullabilityStrategy.NeverNullStrategy, - val optionalStrategy: OptionalStrategy = OptionalStrategy.AlwaysOptionalStrategy, - val recursionStrategy: RecursionStrategy = RecursionStrategy.NullRecursionStrategy, + val random: Random = Random.Default, + val resolvers: List = defaultResolvers, + val strategies: Map, Any> = emptyMap(), ) { - internal fun toSomeConfig(): SomeConfig { - val effectiveNullableStrategy: NullableStrategy = when { - recursionStrategy is RecursionStrategy.NullRecursionStrategy && - nullabilityStrategy is NullabilityStrategy.NeverNullStrategy -> NullableStrategy.NullOnCircularReference - else -> nullabilityStrategy.toSomeStrategy() - } - - val mappedPropertyFactories = propertyFactories.mapValues { (_, generatorLambda) -> - val contextLambda: FixtureContext.() -> Any? = { - val ctx = this - val generator = object : Generator { - override val random: Random get() = ctx.random - - override fun resolveType(type: KType): R { - val res = ctx.resolver ?: error("Resolver not available in FixtureContext") - @Suppress("UNCHECKED_CAST") - return res(type) as R - } - - override fun resolveRange(iterable: Iterable, type: KType): R { - val list = iterable.toList() - return if (list.isNotEmpty()) { - list[random.nextInt(list.size)] - } else { - resolveType(type) - } - } - } - generatorLambda(generator) - } - contextLambda - } - - return SomeConfig( - strategies = mapOf( - NullableStrategy::class to effectiveNullableStrategy, - DefaultValueStrategy::class to optionalStrategy.toSomeStrategy(), - ), - propertyFactories = mappedPropertyFactories, - ) + companion object { + private val defaultRepeatCount: () -> Int = { 5 } } } diff --git a/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/Generator.kt b/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/Generator.kt index c19322f7..284d9bb8 100644 --- a/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/Generator.kt +++ b/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/Generator.kt @@ -4,32 +4,12 @@ import kotlin.random.Random import kotlin.reflect.KType import kotlin.reflect.typeOf -/** - * Receiver interface for compatibility factory blocks. - */ -interface Generator { - /** - * Shared random source for generating fixture values. - */ +interface Generator { val random: Random - - /** - * Resolves a fixture value for [type] through the active resolver chain. - */ - fun resolveType(type: KType): R - - /** - * Selects a value from [iterable] or falls back to resolving a value for [type] when [iterable] is empty. - */ - fun resolveRange(iterable: Iterable, type: KType): R + val fixture: Fixture } -/** - * Resolves a fixture value of type [R] through the active resolver chain. - */ -inline fun Generator<*>.fixture(): R = resolveType(typeOf()) +internal typealias GeneratorFun = Generator.() -> Any? -/** - * Selects a random element from [iterable], falling back to generating a fixture of type [R] if empty. - */ -inline fun Generator<*>.range(iterable: Iterable): R = resolveRange(iterable, typeOf()) +fun Generator.range(range: Iterable) = + range.shuffled(random).firstOrNull() ?: throw NoSuchElementException("Range is empty") \ No newline at end of file From 08bb134ee3b1a5cced8c49ea3ce6fadca51f36fc Mon Sep 17 00:00:00 2001 From: Messias Junior Date: Fri, 18 Sep 2026 17:06:04 +0100 Subject: [PATCH 07/10] property migration --- .../compat/kotlinfixture/Configuration.kt | 25 ++ .../kotlinfixture/ConfigurationBuilder.kt | 336 ++++++++++++++---- .../compat/kotlinfixture/FilterBuilder.kt | 39 -- .../some/compat/kotlinfixture/Fixture.kt | 160 +++------ .../compat/kotlinfixture/KotlinFixture.kt | 11 - 5 files changed, 339 insertions(+), 232 deletions(-) delete mode 100644 kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/FilterBuilder.kt delete mode 100644 kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/KotlinFixture.kt diff --git a/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/Configuration.kt b/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/Configuration.kt index ad7e6ad6..f9c595ee 100644 --- a/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/Configuration.kt +++ b/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/Configuration.kt @@ -1,10 +1,13 @@ package dev.appoutlet.some.compat.kotlinfixture +import dev.appoutlet.some.config.CollectionStrategy +import dev.appoutlet.some.config.SomeConfigBuilder import dev.appoutlet.some.core.Resolver import dev.appoutlet.some.defaultResolvers import kotlin.random.Random import kotlin.reflect.KClass import kotlin.reflect.KType +import kotlin.reflect.full.declaredMemberProperties /** * Immutable configuration for compatibility fixture generation. @@ -23,3 +26,25 @@ data class Configuration( private val defaultRepeatCount: () -> Int = { 5 } } } + + fun SomeConfigBuilder.parseConfiguration(configuration: Configuration, fixture: Fixture) { + strategy(CollectionStrategy(configuration.repeatCount())) + setupProperties(configuration, fixture) +} + +fun SomeConfigBuilder.setupProperties(configuration: Configuration, fixture: Fixture) { + configuration.properties.forEach { (klass, propertyGenerator) -> + propertyGenerator.forEach { (name, function) -> + val generator = object : Generator { + override val random = configuration.random + override val fixture = fixture + } + + val kProperty = klass.declaredMemberProperties.find { it.name == name } + + property(kProperty) { + function(generator) + } + } + } +} \ No newline at end of file diff --git a/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/ConfigurationBuilder.kt b/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/ConfigurationBuilder.kt index c77e0487..c75353de 100644 --- a/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/ConfigurationBuilder.kt +++ b/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/ConfigurationBuilder.kt @@ -1,127 +1,317 @@ package dev.appoutlet.some.compat.kotlinfixture +import dev.appoutlet.some.core.Resolver import kotlin.random.Random -import kotlin.reflect.KCallable import kotlin.reflect.KClass +import kotlin.reflect.KFunction +import kotlin.reflect.KMutableProperty1 import kotlin.reflect.KParameter -import kotlin.reflect.KProperty import kotlin.reflect.KProperty1 import kotlin.reflect.KType -import kotlin.reflect.jvm.javaField -import kotlin.reflect.jvm.javaGetter import kotlin.reflect.typeOf /** - * DSL builder for compatibility fixture configuration. + * Builder of [Configuration]. When [initial] is supplied it is built upon, allowing the various options to be set and + * overridden. */ -class ConfigurationBuilder internal constructor(initial: Configuration = Configuration()) { +@Suppress("TooManyFunctions") +class ConfigurationBuilder(initial: Configuration = Configuration()) { + + /** + * Add and remove [Resolver] to the resolver chain. + * + * We ask each [Resolver] if it handles the current input object, and it returns either a generated fixture or + * `null`. + */ + var resolvers: MutableList = initial.resolvers.toMutableList() + + /** + * Providing a seeded random. + * + * By default, we generate unique values between runs using a default Random class. If you want repeatability you + * can specify a seeded Random instance. + * + * ``` + * val fixture = kotlinFixture { + * random = Random(seed = 10) + * } + * + * val alwaysTheSame = fixture() + * ``` + */ var random: Random = initial.random - var nullabilityStrategy: NullabilityStrategy = initial.nullabilityStrategy - var optionalStrategy: OptionalStrategy = initial.optionalStrategy - var recursionStrategy: RecursionStrategy = initial.recursionStrategy - @PublishedApi - internal val typeFactories = initial.typeFactories.toMutableMap() + /** + * Strategy controlling how nullable types are generated. + */ + var nullabilityStrategy: NullabilityStrategy = + initial.strategies[NullabilityStrategy::class] as? NullabilityStrategy + ?: NullabilityStrategy.NeverNullStrategy - @PublishedApi - internal val propertyFactories = initial.propertyFactories.toMutableMap() + /** + * Strategy controlling whether constructor parameters with default values use the default or a generated value. + */ + var optionalStrategy: OptionalStrategy = + initial.strategies[OptionalStrategy::class] as? OptionalStrategy + ?: OptionalStrategy.AlwaysOptionalStrategy - @PublishedApi - internal val subTypes = initial.subTypes.toMutableMap() + /** + * Strategy controlling how circular references are handled. + */ + var recursionStrategy: RecursionStrategy = + initial.strategies[RecursionStrategy::class] as? RecursionStrategy + ?: RecursionStrategy.NullRecursionStrategy + + private var repeatCount: () -> Int = initial.repeatCount + private val propertiesRepeatCount: MutableMap, MutableMap Int>> = + initial.propertiesRepeatCount.mapValues { it.value.toMutableMap() }.toMutableMap() + private val properties: MutableMap, MutableMap> = + initial.properties.mapValues { it.value.toMutableMap() }.toMutableMap() + private val factories: MutableMap = initial.factories.toMutableMap() + private val subTypes: MutableMap, KClass<*>> = initial.subTypes.toMutableMap() - @PublishedApi - internal val filters = initial.filters.toMutableMap() + internal val strategies: MutableMap, Any> = initial.strategies.toMutableMap() /** - * Registers a custom type factory for type [T]. + * Setting list and map length with `repeatCount`. + * + * Used to determine the length used for lists and maps. By default, the library generates 5 items. + * + * ``` + * val fixture = kotlinFixture { + * repeatCount { 3 } + * } + * + * val listOfThreeItems = fixture>() + * ``` + * + * `repeatCount` is a factory method so can be used to return lists and maps of different lengths each execution: + * + * ``` + * repeatCount { + * random.nextInt(1, 5) + * } + * ``` */ - inline fun factory(noinline factory: Generator.() -> T) { - @Suppress("UNCHECKED_CAST") - typeFactories[typeOf()] = factory as Generator.() -> Any? + fun repeatCount(generator: () -> Int) { + repeatCount = generator } /** - * Registers a custom type factory for [type]. + * Overrides the length used for lists and maps of the property [propertyName] declared by [T]. */ - fun factory(type: KType, factory: Generator.() -> Any?) { - typeFactories[type] = factory - } + @Suppress("UNCHECKED_CAST", "DEPRECATION_ERROR") + inline fun repeatCount( + propertyName: String, + noinline generator: () -> Int + ) = repeatCount(T::class, propertyName, generator) /** - * Registers a custom property factory for [property]. + * Overrides the length used for lists and maps of [property]. */ - fun property(property: KProperty1, factory: Generator.() -> V) { - val (ownerClass, name) = extractOwnerAndName(property) - @Suppress("UNCHECKED_CAST") - propertyFactories[ownerClass to name] = factory as Generator.() -> Any? + inline fun repeatCount( + property: KProperty1, + noinline generator: () -> Int + ) { + if (property !is KMutableProperty1) { + val constructorParams = T::class.constructors.flatMap { + it.parameters.map(KParameter::name) + } + + check(constructorParams.contains(property.name)) { + "No setter available for ${T::class.qualifiedName}.${property.name}" + } + } + + @Suppress("UNCHECKED_CAST", "DEPRECATION_ERROR") + repeatCount(T::class, property.name, generator) } /** - * Registers a custom property factory for [property]. + * Overrides the length used for lists and maps of the property backing the setter [function]. */ - fun property(property: KProperty, factory: Generator.() -> V) { - val (ownerClass, name) = extractOwnerAndName(property) - @Suppress("UNCHECKED_CAST") - propertyFactories[ownerClass to name] = factory as Generator.() -> Any? + @Suppress("UNCHECKED_CAST", "DEPRECATION_ERROR") + fun repeatCount( + function: KFunction, + generator: () -> Int + ) = repeatCount( + function.parameters[0].type.classifier as KClass<*>, + function.name, + generator + ) + + @Deprecated( + "Use one of the repeatCount(Class::property) { … }, repeatCount(propertyName) { … } or " + + "repeatCount(Class::function) { … } functions", + level = DeprecationLevel.ERROR + ) + fun repeatCount(clazz: KClass<*>, propertyName: String, generator: () -> Int) { + val classProperties = propertiesRepeatCount.getOrElse(clazz) { mutableMapOf() } + classProperties[propertyName] = generator + + propertiesRepeatCount[clazz] = classProperties } /** - * Maps superclass [Super] to subtype [Sub]. + * Customising class generation with factory. + * + * Used to return the given instance for a particular class using a factory method. + * + * ``` + * val fixture = kotlinFixture { + * factory { 41 } + * } + * + * val alwaysFortyOne = fixture() + * ``` + * + * As `factory` is a factory method you can return different values on every execution: + * + * ``` + * factory { + * random.nextInt(10, 50) + * } + * ``` + * + * #### Generating values in a `range` + * + * `factory` has a built-in `range` function to make it easy to generate values in a range. + * + * ``` + * factory { range(1..10) } + * ``` */ - inline fun subType() { - subType(Super::class, Sub::class) + @Suppress("UNCHECKED_CAST", "DEPRECATION_ERROR") + inline fun factory(noinline generator: Generator.() -> T) = + factory(typeOf(), generator as GeneratorFun) + + @Deprecated("Use the factory { … } function", level = DeprecationLevel.ERROR) + fun factory(type: KType, generator: GeneratorFun) { + factories[type] = generator } /** - * Maps [superType] to [subType]. + * Resolving abstract superclasses to a chosen subclass with `subType`. + * + * Used to always return an instance of a particular subclass for a superclass. + * + * ``` + * val fixture = kotlinFixture { + * subType() + * } + * + * val alwaysInt = fixture() + * ``` */ + @Suppress("DEPRECATION_ERROR") + inline fun subType() = subType(T::class, U::class) + + @Deprecated( + "Use the subType() function instead", + level = DeprecationLevel.ERROR + ) fun subType(superType: KClass<*>, subType: KClass<*>) { subTypes[superType] = subType } /** - * Configures a filter for type [T]. + * Customising generation of class properties with property. + * + * Used to override constructor parameters or mutable properties when generating instances of generic classes. + * + * ``` + * val fixture = kotlinFixture { + * // Public constructor parameters overridden by reference: + * property(KotlinClass::readOnly) { "a" } + * + * // Private constructor parameters are overridden by name: + * property("private") { "b" } + * + * // Public member properties overridden by reference: + * property(KotlinClass::member) { "c" } + * } + * ``` */ - inline fun filter(noinline configuration: FilterBuilder.() -> Unit) { - filter(typeOf(), configuration) - } + @Suppress("UNCHECKED_CAST", "DEPRECATION_ERROR") + inline fun property( + propertyName: String, + noinline generator: Generator.() -> G + ) = property(T::class, propertyName, generator as GeneratorFun) /** - * Configures a filter for [type]. + * Customising generation of class properties with property. + * + * Used to override constructor parameters or mutable properties when generating instances of generic classes. + * + * ``` + * val fixture = kotlinFixture { + * property(KotlinClass::readOnly) { "a" } + * } + * ``` */ - @Suppress("UNCHECKED_CAST") - fun filter(type: KType, configuration: FilterBuilder.() -> Unit) { - val builder = FilterBuilder().apply(configuration) - filters[type] = builder.build() as FilterSpec - } + inline fun property( + property: KProperty1, + noinline generator: Generator.() -> G + ) { + if (property !is KMutableProperty1) { + val constructorParams = T::class.constructors.flatMap { + it.parameters.map(KParameter::name) + } - internal fun build(): Configuration = Configuration( - random = random, - typeFactories = typeFactories.toMap(), - propertyFactories = propertyFactories.toMap(), - subTypes = subTypes.toMap(), - filters = filters.toMap(), - nullabilityStrategy = nullabilityStrategy, - optionalStrategy = optionalStrategy, - recursionStrategy = recursionStrategy, - ) - - private fun extractOwnerAndName(property: KProperty<*>): Pair, String> { - val propertyName = property.name + check(constructorParams.contains(property.name)) { + "No setter available for ${T::class.qualifiedName}.${property.name}" + } + } - val instanceParamClass = (property as? KCallable<*>)?.parameters - ?.firstOrNull { it.kind == KParameter.Kind.INSTANCE || it.kind == KParameter.Kind.EXTENSION_RECEIVER } - ?.type?.classifier as? KClass<*> + @Suppress("UNCHECKED_CAST", "DEPRECATION_ERROR") + property(T::class, property.name, generator as GeneratorFun) + } - val ownerClass = instanceParamClass - ?: (property as? KProperty1<*, *>)?.javaGetter?.declaringClass?.kotlin - ?: (property as? KProperty1<*, *>)?.javaField?.declaringClass?.kotlin + /** + * Customising generation of class properties with property. + * + * Used to override constructor parameters or mutable properties when generating instances of generic classes. + * + * ``` + * val fixture = kotlinFixture { + * property(JavaClass::setMutable) { "d" } + * } + * ``` + */ + @Suppress("UNCHECKED_CAST", "DEPRECATION_ERROR") + inline fun property( + function: KFunction, + noinline generator: Generator.() -> G + ) = property( + function.parameters[0].type.classifier as KClass<*>, + function.name, + generator as GeneratorFun + ) - requireNotNull(ownerClass) { - "Could not determine declaring class for property '${property.name}'. " + - "Unsupported private or non-constructor property." - } + @Deprecated( + "Use one of the property(Class::property) { … }, property(propertyName) { … } or " + + "property(Class::function) { … } functions", + level = DeprecationLevel.ERROR + ) + fun property(clazz: KClass<*>, propertyName: String, generator: GeneratorFun) { + val classProperties = properties.getOrElse(clazz) { mutableMapOf() } + classProperties[propertyName] = generator - return ownerClass to propertyName + properties[clazz] = classProperties } + + fun build() = Configuration( + repeatCount = repeatCount, + propertiesRepeatCount = propertiesRepeatCount.mapValues { it.value.toMap() }.toMap(), + properties = properties.mapValues { it.value.toMap() }.toMap(), + factories = factories.toMap(), + subTypes = subTypes.toMap(), + random = random, + resolvers = resolvers.toList(), + strategies = strategies.toMutableMap().apply { + this[NullabilityStrategy::class] = nullabilityStrategy + this[OptionalStrategy::class] = optionalStrategy + this[RecursionStrategy::class] = recursionStrategy + }.toMap(), + ) } diff --git a/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/FilterBuilder.kt b/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/FilterBuilder.kt deleted file mode 100644 index afddc517..00000000 --- a/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/FilterBuilder.kt +++ /dev/null @@ -1,39 +0,0 @@ -package dev.appoutlet.some.compat.kotlinfixture - -/** - * DSL builder for configuring type filters. - */ -class FilterBuilder { - internal val predicates = mutableListOf<(T) -> Boolean>() - internal var isDistinct: Boolean = false - - /** - * Adds a filter predicate that candidates must satisfy. - */ - fun filter(predicate: (T) -> Boolean) { - predicates.add(predicate) - } - - /** - * Specifies that generated values for this type must be distinct. - */ - fun distinct() { - isDistinct = true - } - - internal fun build(): FilterSpec = FilterSpec(predicates.toList(), isDistinct) -} - -/** - * Specification for filtered type generation. - */ -data class FilterSpec( - val predicates: List<(T) -> Boolean>, - val isDistinct: Boolean -) { - fun matchesPredicate(value: T): Boolean = predicates.all { it(value) } - - fun matches(value: T, seen: Set): Boolean { - return matchesPredicate(value) && (!isDistinct || value !in seen) - } -} 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 index de9167ab..6d1d13b3 100644 --- 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 @@ -1,140 +1,82 @@ package dev.appoutlet.some.compat.kotlinfixture -import dev.appoutlet.some.Some -import dev.appoutlet.some.config.NullableStrategy -import dev.appoutlet.some.core.ResolverChain +import dev.appoutlet.some.some +import kotlin.reflect.KClass import kotlin.reflect.KType import kotlin.reflect.typeOf -private const val MAX_FILTER_ATTEMPTS = 10000 /** * Main compatibility generator instance. */ -class Fixture internal constructor( - val configuration: Configuration -) { - @PublishedApi - internal val someInstance: Some = buildSomeInstance(configuration) - - /** - * Generates a fixture value of type [T]. - * - * @param range Iterable to select a value from when non-empty. - * @param configuration Optional per-call configuration overrides. - */ +class Fixture @JvmOverloads constructor(val fixtureConfiguration: Configuration = ConfigurationBuilder().build()) { inline operator fun invoke( range: Iterable = emptyList(), - noinline configuration: (ConfigurationBuilder.() -> Unit)? = null + noinline configuration: ConfigurationBuilder.() -> Unit = {} ): T { - val targetFixture = if (configuration != null) new(configuration) else this - @Suppress("UNCHECKED_CAST") - return targetFixture.generateValue(typeOf(), range) as T + val rangeShuffled = range.shuffled() + return if (rangeShuffled.isNotEmpty()) { + rangeShuffled.first() + } else { + val currentConfiguration = ConfigurationBuilder(fixtureConfiguration) + .apply(configuration) + .build() + + some { parseConfiguration(currentConfiguration, this@Fixture) } + } } /** - * Generates a sequence of fixture values of type [T]. - * - * @param sequenceStrategy Strategy controlling sequence bounding. - * @param configuration Optional per-call configuration overrides. + * Create a sequence of instances of [T] with the option to override any [configuration] for all instances + * generated by this sequence. */ inline fun asSequence( - sequenceStrategy: SequenceStrategy = SequenceStrategy.Unbounded, - noinline configuration: (ConfigurationBuilder.() -> Unit)? = null + noinline configuration: ConfigurationBuilder.() -> Unit = {} ): Sequence { - val targetFixture = if (configuration != null) new(configuration) else this - @Suppress("UNCHECKED_CAST") - return targetFixture.generateSequence(typeOf(), sequenceStrategy) as Sequence + val type = typeOf() + val builtConfiguration = ConfigurationBuilder(fixtureConfiguration).apply(configuration).build() + return sequence { + while (true) { + @Suppress("DEPRECATION_ERROR") + yield(create(type, builtConfiguration) as T) + } + } } /** - * Creates a new [Fixture] deriving from this instance's configuration. + * Create an instance of [clazz] with the option to override any [configuration] specifically for this instance. */ - fun new( - configuration: ConfigurationBuilder.() -> Unit = {} - ): Fixture { - val newConfig = ConfigurationBuilder(this.configuration).apply(configuration).build() - return Fixture(newConfig) + @JvmOverloads + fun create(clazz: Class<*>, configuration: Configuration = fixtureConfiguration): Any? { + @Suppress("DEPRECATION_ERROR") + return create(clazz.kotlin, configuration) } - @PublishedApi - internal fun generateValue(type: KType, range: Iterable): Any? { - val rangeList = range.toList() - val filterSpec = configuration.filters[type] - - if (rangeList.isNotEmpty()) { - val eligible = if (filterSpec != null) { - rangeList.filter { filterSpec.matchesPredicate(it) } - } else { - rangeList - } - - if (eligible.isNotEmpty()) { - return eligible[configuration.random.nextInt(eligible.size)] - } - } - - return generateSingleRawValue(type, filterSpec) + @Deprecated("Use the fixture() function", level = DeprecationLevel.ERROR) + fun create(clazz: KClass<*>, configuration: Configuration = fixtureConfiguration): Any? { + @Suppress("DEPRECATION_ERROR") + return create(clazz.starProjectedType, configuration) } - private fun generateSingleRawValue(type: KType, filterSpec: FilterSpec?): Any? { - if (filterSpec == null) { - return rawResolve(type) - } - - val seen = mutableSetOf() - var attempts = 0 - - while (attempts < MAX_FILTER_ATTEMPTS) { - attempts++ - val candidate = rawResolve(type) - if (filterSpec.matches(candidate, seen)) { - if (filterSpec.isDistinct) seen.add(candidate) - return candidate - } + @Deprecated("Use the fixture() function", level = DeprecationLevel.ERROR) + fun create(type: KType, configuration: Configuration = fixtureConfiguration): Any? { + val result = ContextImpl(configuration).resolve(type) + if (result is Unresolved) { + throw UnsupportedOperationException("Unable to handle $type\n$result") } - - error("Filter could not be satisfied after $MAX_FILTER_ATTEMPTS attempts for type $type") + return result } - @PublishedApi - internal fun generateSequence(type: KType, sequenceStrategy: SequenceStrategy): Sequence { - val filterSpec = configuration.filters[type] - - val rawSeq = kotlin.sequences.generateSequence { rawResolve(type) } - .filter { candidate -> filterSpec?.matchesPredicate(candidate) ?: true } - .let { seq -> - if (filterSpec?.isDistinct == true) seq.distinct() else seq - } - - return when (sequenceStrategy) { - is SequenceStrategy.Unbounded -> rawSeq - is SequenceStrategy.Bounded -> { - if (sequenceStrategy.numberOfElements == 0) { - emptySequence() - } else { - rawSeq.take(sequenceStrategy.numberOfElements) - } - } - } - } - - private fun rawResolve(type: KType): Any? { - val nullableStrategy = someInstance.config[NullableStrategy::class] - val session = ResolverChain(someInstance.resolvers, nullableStrategy) - return session.resolve(type) - } - - companion object { - private fun buildSomeInstance(config: Configuration): Some { - val someConfig = config.toSomeConfig() - val random = config.random - val nativeResolvers = someConfig.buildResolvers(random) - val customFactoryResolver = CompatibilityFactoryResolver(config.typeFactories, random) - val subtypeResolver = CompatibilitySubtypeResolver(config.subTypes) - - val combinedResolvers = listOf(customFactoryResolver, subtypeResolver) + nativeResolvers - return Some(combinedResolvers, random, someConfig) - } + /** + * Generate a new [Fixture] with overridden configuration. + */ + fun new(configuration: ConfigurationBuilder.() -> Unit = {}): Fixture { + return Fixture(ConfigurationBuilder(fixtureConfiguration).apply(configuration).build()) } } + +/** + * Create a [Fixture] with [configuration] + */ +fun kotlinFixture(configuration: ConfigurationBuilder.() -> Unit = {}) = + Fixture(ConfigurationBuilder().apply(configuration).build()) \ No newline at end of file diff --git a/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/KotlinFixture.kt b/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/KotlinFixture.kt deleted file mode 100644 index 3856f6eb..00000000 --- a/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/KotlinFixture.kt +++ /dev/null @@ -1,11 +0,0 @@ -package dev.appoutlet.some.compat.kotlinfixture - -/** - * Creates a compatibility [Fixture] generator configured with [configuration]. - */ -fun kotlinFixture( - configuration: ConfigurationBuilder.() -> Unit = {} -): Fixture { - val builder = ConfigurationBuilder().apply(configuration) - return Fixture(builder.build()) -} From 1992049816178b78b9ee144d5aa3849dcb171920 Mon Sep 17 00:00:00 2001 From: Messias Junior Date: Fri, 18 Sep 2026 17:35:18 +0100 Subject: [PATCH 08/10] simplify integration --- .../compat/kotlinfixture/Configuration.kt | 50 --- .../kotlinfixture/ConfigurationBuilder.kt | 317 ------------------ .../some/compat/kotlinfixture/Fixture.kt | 86 ++--- .../some/compat/kotlinfixture/Generator.kt | 15 - .../kotlinfixture/NullabilityStrategy.kt | 26 -- .../compat/kotlinfixture/OptionalStrategy.kt | 26 -- .../compat/kotlinfixture/RecursionStrategy.kt | 22 -- 7 files changed, 37 insertions(+), 505 deletions(-) delete mode 100644 kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/Configuration.kt delete mode 100644 kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/ConfigurationBuilder.kt delete mode 100644 kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/Generator.kt delete mode 100644 kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/NullabilityStrategy.kt delete mode 100644 kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/OptionalStrategy.kt delete mode 100644 kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/RecursionStrategy.kt diff --git a/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/Configuration.kt b/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/Configuration.kt deleted file mode 100644 index f9c595ee..00000000 --- a/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/Configuration.kt +++ /dev/null @@ -1,50 +0,0 @@ -package dev.appoutlet.some.compat.kotlinfixture - -import dev.appoutlet.some.config.CollectionStrategy -import dev.appoutlet.some.config.SomeConfigBuilder -import dev.appoutlet.some.core.Resolver -import dev.appoutlet.some.defaultResolvers -import kotlin.random.Random -import kotlin.reflect.KClass -import kotlin.reflect.KType -import kotlin.reflect.full.declaredMemberProperties - -/** - * Immutable configuration for compatibility fixture generation. - */ -data class Configuration( - val repeatCount: () -> Int = defaultRepeatCount, - val propertiesRepeatCount: Map, Map Int>> = emptyMap, Map Int>>(), - val properties: Map, Map> = emptyMap(), - val factories: Map = emptyMap(), - val subTypes: Map, KClass<*>> = emptyMap(), - val random: Random = Random.Default, - val resolvers: List = defaultResolvers, - val strategies: Map, Any> = emptyMap(), -) { - companion object { - private val defaultRepeatCount: () -> Int = { 5 } - } -} - - fun SomeConfigBuilder.parseConfiguration(configuration: Configuration, fixture: Fixture) { - strategy(CollectionStrategy(configuration.repeatCount())) - setupProperties(configuration, fixture) -} - -fun SomeConfigBuilder.setupProperties(configuration: Configuration, fixture: Fixture) { - configuration.properties.forEach { (klass, propertyGenerator) -> - propertyGenerator.forEach { (name, function) -> - val generator = object : Generator { - override val random = configuration.random - override val fixture = fixture - } - - val kProperty = klass.declaredMemberProperties.find { it.name == name } - - property(kProperty) { - function(generator) - } - } - } -} \ No newline at end of file diff --git a/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/ConfigurationBuilder.kt b/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/ConfigurationBuilder.kt deleted file mode 100644 index c75353de..00000000 --- a/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/ConfigurationBuilder.kt +++ /dev/null @@ -1,317 +0,0 @@ -package dev.appoutlet.some.compat.kotlinfixture - -import dev.appoutlet.some.core.Resolver -import kotlin.random.Random -import kotlin.reflect.KClass -import kotlin.reflect.KFunction -import kotlin.reflect.KMutableProperty1 -import kotlin.reflect.KParameter -import kotlin.reflect.KProperty1 -import kotlin.reflect.KType -import kotlin.reflect.typeOf - -/** - * Builder of [Configuration]. When [initial] is supplied it is built upon, allowing the various options to be set and - * overridden. - */ -@Suppress("TooManyFunctions") -class ConfigurationBuilder(initial: Configuration = Configuration()) { - - /** - * Add and remove [Resolver] to the resolver chain. - * - * We ask each [Resolver] if it handles the current input object, and it returns either a generated fixture or - * `null`. - */ - var resolvers: MutableList = initial.resolvers.toMutableList() - - /** - * Providing a seeded random. - * - * By default, we generate unique values between runs using a default Random class. If you want repeatability you - * can specify a seeded Random instance. - * - * ``` - * val fixture = kotlinFixture { - * random = Random(seed = 10) - * } - * - * val alwaysTheSame = fixture() - * ``` - */ - var random: Random = initial.random - - /** - * Strategy controlling how nullable types are generated. - */ - var nullabilityStrategy: NullabilityStrategy = - initial.strategies[NullabilityStrategy::class] as? NullabilityStrategy - ?: NullabilityStrategy.NeverNullStrategy - - /** - * Strategy controlling whether constructor parameters with default values use the default or a generated value. - */ - var optionalStrategy: OptionalStrategy = - initial.strategies[OptionalStrategy::class] as? OptionalStrategy - ?: OptionalStrategy.AlwaysOptionalStrategy - - /** - * Strategy controlling how circular references are handled. - */ - var recursionStrategy: RecursionStrategy = - initial.strategies[RecursionStrategy::class] as? RecursionStrategy - ?: RecursionStrategy.NullRecursionStrategy - - private var repeatCount: () -> Int = initial.repeatCount - private val propertiesRepeatCount: MutableMap, MutableMap Int>> = - initial.propertiesRepeatCount.mapValues { it.value.toMutableMap() }.toMutableMap() - private val properties: MutableMap, MutableMap> = - initial.properties.mapValues { it.value.toMutableMap() }.toMutableMap() - private val factories: MutableMap = initial.factories.toMutableMap() - private val subTypes: MutableMap, KClass<*>> = initial.subTypes.toMutableMap() - - internal val strategies: MutableMap, Any> = initial.strategies.toMutableMap() - - /** - * Setting list and map length with `repeatCount`. - * - * Used to determine the length used for lists and maps. By default, the library generates 5 items. - * - * ``` - * val fixture = kotlinFixture { - * repeatCount { 3 } - * } - * - * val listOfThreeItems = fixture>() - * ``` - * - * `repeatCount` is a factory method so can be used to return lists and maps of different lengths each execution: - * - * ``` - * repeatCount { - * random.nextInt(1, 5) - * } - * ``` - */ - fun repeatCount(generator: () -> Int) { - repeatCount = generator - } - - /** - * Overrides the length used for lists and maps of the property [propertyName] declared by [T]. - */ - @Suppress("UNCHECKED_CAST", "DEPRECATION_ERROR") - inline fun repeatCount( - propertyName: String, - noinline generator: () -> Int - ) = repeatCount(T::class, propertyName, generator) - - /** - * Overrides the length used for lists and maps of [property]. - */ - inline fun repeatCount( - property: KProperty1, - noinline generator: () -> Int - ) { - if (property !is KMutableProperty1) { - val constructorParams = T::class.constructors.flatMap { - it.parameters.map(KParameter::name) - } - - check(constructorParams.contains(property.name)) { - "No setter available for ${T::class.qualifiedName}.${property.name}" - } - } - - @Suppress("UNCHECKED_CAST", "DEPRECATION_ERROR") - repeatCount(T::class, property.name, generator) - } - - /** - * Overrides the length used for lists and maps of the property backing the setter [function]. - */ - @Suppress("UNCHECKED_CAST", "DEPRECATION_ERROR") - fun repeatCount( - function: KFunction, - generator: () -> Int - ) = repeatCount( - function.parameters[0].type.classifier as KClass<*>, - function.name, - generator - ) - - @Deprecated( - "Use one of the repeatCount(Class::property) { … }, repeatCount(propertyName) { … } or " + - "repeatCount(Class::function) { … } functions", - level = DeprecationLevel.ERROR - ) - fun repeatCount(clazz: KClass<*>, propertyName: String, generator: () -> Int) { - val classProperties = propertiesRepeatCount.getOrElse(clazz) { mutableMapOf() } - classProperties[propertyName] = generator - - propertiesRepeatCount[clazz] = classProperties - } - - /** - * Customising class generation with factory. - * - * Used to return the given instance for a particular class using a factory method. - * - * ``` - * val fixture = kotlinFixture { - * factory { 41 } - * } - * - * val alwaysFortyOne = fixture() - * ``` - * - * As `factory` is a factory method you can return different values on every execution: - * - * ``` - * factory { - * random.nextInt(10, 50) - * } - * ``` - * - * #### Generating values in a `range` - * - * `factory` has a built-in `range` function to make it easy to generate values in a range. - * - * ``` - * factory { range(1..10) } - * ``` - */ - @Suppress("UNCHECKED_CAST", "DEPRECATION_ERROR") - inline fun factory(noinline generator: Generator.() -> T) = - factory(typeOf(), generator as GeneratorFun) - - @Deprecated("Use the factory { … } function", level = DeprecationLevel.ERROR) - fun factory(type: KType, generator: GeneratorFun) { - factories[type] = generator - } - - /** - * Resolving abstract superclasses to a chosen subclass with `subType`. - * - * Used to always return an instance of a particular subclass for a superclass. - * - * ``` - * val fixture = kotlinFixture { - * subType() - * } - * - * val alwaysInt = fixture() - * ``` - */ - @Suppress("DEPRECATION_ERROR") - inline fun subType() = subType(T::class, U::class) - - @Deprecated( - "Use the subType() function instead", - level = DeprecationLevel.ERROR - ) - fun subType(superType: KClass<*>, subType: KClass<*>) { - subTypes[superType] = subType - } - - /** - * Customising generation of class properties with property. - * - * Used to override constructor parameters or mutable properties when generating instances of generic classes. - * - * ``` - * val fixture = kotlinFixture { - * // Public constructor parameters overridden by reference: - * property(KotlinClass::readOnly) { "a" } - * - * // Private constructor parameters are overridden by name: - * property("private") { "b" } - * - * // Public member properties overridden by reference: - * property(KotlinClass::member) { "c" } - * } - * ``` - */ - @Suppress("UNCHECKED_CAST", "DEPRECATION_ERROR") - inline fun property( - propertyName: String, - noinline generator: Generator.() -> G - ) = property(T::class, propertyName, generator as GeneratorFun) - - /** - * Customising generation of class properties with property. - * - * Used to override constructor parameters or mutable properties when generating instances of generic classes. - * - * ``` - * val fixture = kotlinFixture { - * property(KotlinClass::readOnly) { "a" } - * } - * ``` - */ - inline fun property( - property: KProperty1, - noinline generator: Generator.() -> G - ) { - if (property !is KMutableProperty1) { - val constructorParams = T::class.constructors.flatMap { - it.parameters.map(KParameter::name) - } - - check(constructorParams.contains(property.name)) { - "No setter available for ${T::class.qualifiedName}.${property.name}" - } - } - - @Suppress("UNCHECKED_CAST", "DEPRECATION_ERROR") - property(T::class, property.name, generator as GeneratorFun) - } - - /** - * Customising generation of class properties with property. - * - * Used to override constructor parameters or mutable properties when generating instances of generic classes. - * - * ``` - * val fixture = kotlinFixture { - * property(JavaClass::setMutable) { "d" } - * } - * ``` - */ - @Suppress("UNCHECKED_CAST", "DEPRECATION_ERROR") - inline fun property( - function: KFunction, - noinline generator: Generator.() -> G - ) = property( - function.parameters[0].type.classifier as KClass<*>, - function.name, - generator as GeneratorFun - ) - - @Deprecated( - "Use one of the property(Class::property) { … }, property(propertyName) { … } or " + - "property(Class::function) { … } functions", - level = DeprecationLevel.ERROR - ) - fun property(clazz: KClass<*>, propertyName: String, generator: GeneratorFun) { - val classProperties = properties.getOrElse(clazz) { mutableMapOf() } - classProperties[propertyName] = generator - - properties[clazz] = classProperties - } - - fun build() = Configuration( - repeatCount = repeatCount, - propertiesRepeatCount = propertiesRepeatCount.mapValues { it.value.toMap() }.toMap(), - properties = properties.mapValues { it.value.toMap() }.toMap(), - factories = factories.toMap(), - subTypes = subTypes.toMap(), - random = random, - resolvers = resolvers.toList(), - strategies = strategies.toMutableMap().apply { - this[NullabilityStrategy::class] = nullabilityStrategy - this[OptionalStrategy::class] = optionalStrategy - this[RecursionStrategy::class] = recursionStrategy - }.toMap(), - ) -} 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 index 6d1d13b3..244f7992 100644 --- 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 @@ -1,28 +1,26 @@ package dev.appoutlet.some.compat.kotlinfixture +import dev.appoutlet.some.config.SomeConfigBuilder import dev.appoutlet.some.some -import kotlin.reflect.KClass -import kotlin.reflect.KType -import kotlin.reflect.typeOf /** * Main compatibility generator instance. */ -class Fixture @JvmOverloads constructor(val fixtureConfiguration: Configuration = ConfigurationBuilder().build()) { - inline operator fun invoke( +class Fixture @JvmOverloads constructor(val someConfig: SomeConfigBuilder.() -> Unit = {}) { + + inline operator fun invoke( range: Iterable = emptyList(), - noinline configuration: ConfigurationBuilder.() -> Unit = {} + noinline configuration: SomeConfigBuilder.() -> Unit = {} ): T { val rangeShuffled = range.shuffled() return if (rangeShuffled.isNotEmpty()) { rangeShuffled.first() } else { - val currentConfiguration = ConfigurationBuilder(fixtureConfiguration) - .apply(configuration) - .build() - - some { parseConfiguration(currentConfiguration, this@Fixture) } + some { + this.apply(someConfig) + this.apply(configuration) + } } } @@ -30,53 +28,43 @@ class Fixture @JvmOverloads constructor(val fixtureConfiguration: Configuration * Create a sequence of instances of [T] with the option to override any [configuration] for all instances * generated by this sequence. */ - inline fun asSequence( - noinline configuration: ConfigurationBuilder.() -> Unit = {} + inline fun asSequence( + sequenceStrategy: SequenceStrategy = SequenceStrategy.Unbounded, + noinline configuration: SomeConfigBuilder.() -> Unit = {} ): Sequence { - val type = typeOf() - val builtConfiguration = ConfigurationBuilder(fixtureConfiguration).apply(configuration).build() return sequence { - while (true) { - @Suppress("DEPRECATION_ERROR") - yield(create(type, builtConfiguration) as T) + val value = { + some { + this.apply(someConfig) + this.apply(configuration) + } } - } - } - /** - * Create an instance of [clazz] with the option to override any [configuration] specifically for this instance. - */ - @JvmOverloads - fun create(clazz: Class<*>, configuration: Configuration = fixtureConfiguration): Any? { - @Suppress("DEPRECATION_ERROR") - return create(clazz.kotlin, configuration) - } + when(sequenceStrategy) { + is SequenceStrategy.Bounded -> { + repeat(times = sequenceStrategy.numberOfElements) { + yield(value()) + } + } - @Deprecated("Use the fixture() function", level = DeprecationLevel.ERROR) - fun create(clazz: KClass<*>, configuration: Configuration = fixtureConfiguration): Any? { - @Suppress("DEPRECATION_ERROR") - return create(clazz.starProjectedType, configuration) + SequenceStrategy.Unbounded -> { + while (true) { + yield(value()) + } + } + } + } } - @Deprecated("Use the fixture() function", level = DeprecationLevel.ERROR) - fun create(type: KType, configuration: Configuration = fixtureConfiguration): Any? { - val result = ContextImpl(configuration).resolve(type) - if (result is Unresolved) { - throw UnsupportedOperationException("Unable to handle $type\n$result") - } - return result + @JvmOverloads + inline fun create(noinline configuration: SomeConfigBuilder.() -> Unit = {}): T { + return some(configuration) } - /** - * Generate a new [Fixture] with overridden configuration. - */ - fun new(configuration: ConfigurationBuilder.() -> Unit = {}): Fixture { - return Fixture(ConfigurationBuilder(fixtureConfiguration).apply(configuration).build()) + fun new(configuration: SomeConfigBuilder.() -> Unit = {}): Fixture { + return Fixture(configuration) } } -/** - * Create a [Fixture] with [configuration] - */ -fun kotlinFixture(configuration: ConfigurationBuilder.() -> Unit = {}) = - Fixture(ConfigurationBuilder().apply(configuration).build()) \ No newline at end of file +fun kotlinFixture(configuration: SomeConfigBuilder.() -> Unit = {}) = + Fixture(configuration) diff --git a/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/Generator.kt b/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/Generator.kt deleted file mode 100644 index 284d9bb8..00000000 --- a/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/Generator.kt +++ /dev/null @@ -1,15 +0,0 @@ -package dev.appoutlet.some.compat.kotlinfixture - -import kotlin.random.Random -import kotlin.reflect.KType -import kotlin.reflect.typeOf - -interface Generator { - val random: Random - val fixture: Fixture -} - -internal typealias GeneratorFun = Generator.() -> Any? - -fun Generator.range(range: Iterable) = - range.shuffled(random).firstOrNull() ?: throw NoSuchElementException("Range is empty") \ No newline at end of file diff --git a/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/NullabilityStrategy.kt b/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/NullabilityStrategy.kt deleted file mode 100644 index bd471546..00000000 --- a/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/NullabilityStrategy.kt +++ /dev/null @@ -1,26 +0,0 @@ -package dev.appoutlet.some.compat.kotlinfixture - -import dev.appoutlet.some.config.NullableStrategy - -/** - * Compatibility nullability strategies matching Appmattus style. - */ -sealed interface NullabilityStrategy { - fun toSomeStrategy(): NullableStrategy - - data object NeverNullStrategy : NullabilityStrategy { - override fun toSomeStrategy(): NullableStrategy = NullableStrategy.NeverNull - } - - data object AlwaysNullStrategy : NullabilityStrategy { - override fun toSomeStrategy(): NullableStrategy = NullableStrategy.AlwaysNull - } - - data class RandomlyNullStrategy(val nullability: Float = 0.5f) : NullabilityStrategy { - init { - require(nullability in 0f..1f) { "nullability must be between 0.0 and 1.0" } - } - - override fun toSomeStrategy(): NullableStrategy = NullableStrategy.Random(nullability.toDouble()) - } -} diff --git a/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/OptionalStrategy.kt b/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/OptionalStrategy.kt deleted file mode 100644 index b9fd150c..00000000 --- a/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/OptionalStrategy.kt +++ /dev/null @@ -1,26 +0,0 @@ -package dev.appoutlet.some.compat.kotlinfixture - -import dev.appoutlet.some.config.DefaultValueStrategy - -/** - * Compatibility optional value strategies matching Appmattus style. - */ -sealed interface OptionalStrategy { - fun toSomeStrategy(): DefaultValueStrategy - - data object AlwaysOptionalStrategy : OptionalStrategy { - override fun toSomeStrategy(): DefaultValueStrategy = DefaultValueStrategy.UseDefault - } - - data object NeverOptionalStrategy : OptionalStrategy { - override fun toSomeStrategy(): DefaultValueStrategy = DefaultValueStrategy.Generate - } - - data class RandomlyOptionalStrategy(val optionality: Float = 0.5f) : OptionalStrategy { - init { - require(optionality in 0f..1f) { "optionality must be between 0.0 and 1.0" } - } - - override fun toSomeStrategy(): DefaultValueStrategy = DefaultValueStrategy.Random(optionality) - } -} diff --git a/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/RecursionStrategy.kt b/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/RecursionStrategy.kt deleted file mode 100644 index d6c1fd6c..00000000 --- a/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/RecursionStrategy.kt +++ /dev/null @@ -1,22 +0,0 @@ -package dev.appoutlet.some.compat.kotlinfixture - -import dev.appoutlet.some.config.NullableStrategy - -/** - * Compatibility recursion strategies matching Appmattus style. - */ -sealed interface RecursionStrategy { - fun toSomeStrategy(): NullableStrategy - - data object NullRecursionStrategy : RecursionStrategy { - override fun toSomeStrategy(): NullableStrategy = NullableStrategy.NullOnCircularReference - } - - data object ThrowingRecursionStrategy : RecursionStrategy { - override fun toSomeStrategy(): NullableStrategy = NullableStrategy.NeverNull - } - - data object UnresolvedRecursionStrategy : RecursionStrategy { - override fun toSomeStrategy(): NullableStrategy = NullableStrategy.NeverNull - } -} From 57f92654b52637bdf3f6a561c2249b9c04357660 Mon Sep 17 00:00:00 2001 From: Messias Junior Date: Fri, 18 Sep 2026 17:41:43 +0100 Subject: [PATCH 09/10] rewrite tests --- .../some/config/CollectionStrategy.kt | 2 +- docs/migration/kotlin-fixture-to-some.md | 163 ++++++++++-------- .../some/compat/kotlinfixture/Fixture.kt | 9 +- .../kotlinfixture/ConfigurationBuilderTest.kt | 46 ----- .../compat/kotlinfixture/EdgeCasesTest.kt | 88 ---------- .../kotlinfixture/FactoryAndPropertyTest.kt | 52 ++++++ .../kotlinfixture/FactoryCompatibilityTest.kt | 59 ------- .../kotlinfixture/FilterCompatibilityTest.kt | 36 ---- .../some/compat/kotlinfixture/FixtureTest.kt | 78 +++++---- .../MigrationApiCompilationTest.kt | 30 ---- .../NullableStrategyCompatibilityTest.kt | 47 ----- .../OptionalStrategyCompatibilityTest.kt | 28 --- .../PropertyCompatibilityTest.kt | 30 ---- .../kotlinfixture/RandomCompatibilityTest.kt | 56 ------ .../RecursionStrategyCompatibilityTest.kt | 41 ----- .../kotlinfixture/SequenceStrategyTest.kt | 47 ++--- .../kotlinfixture/SubtypeCompatibilityTest.kt | 41 ----- .../some/compat/kotlinfixture/TestModels.kt | 18 +- 18 files changed, 207 insertions(+), 664 deletions(-) delete mode 100644 kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/ConfigurationBuilderTest.kt delete mode 100644 kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/EdgeCasesTest.kt create mode 100644 kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/FactoryAndPropertyTest.kt delete mode 100644 kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/FactoryCompatibilityTest.kt delete mode 100644 kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/FilterCompatibilityTest.kt delete mode 100644 kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/MigrationApiCompilationTest.kt delete mode 100644 kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/NullableStrategyCompatibilityTest.kt delete mode 100644 kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/OptionalStrategyCompatibilityTest.kt delete mode 100644 kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/PropertyCompatibilityTest.kt delete mode 100644 kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/RandomCompatibilityTest.kt delete mode 100644 kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/RecursionStrategyCompatibilityTest.kt delete mode 100644 kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/SubtypeCompatibilityTest.kt 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 98b5da5c..0b5d4b16 100644 --- a/core/src/main/kotlin/dev/appoutlet/some/config/CollectionStrategy.kt +++ b/core/src/main/kotlin/dev/appoutlet/some/config/CollectionStrategy.kt @@ -14,7 +14,7 @@ data class CollectionStrategy( require(sizeRange.last >= sizeRange.first) { "sizeRange.end must be greater than or equal to sizeRange.start" } } - constructor(size: Int): this(size..size) + constructor(size: Int) : this(size..size) companion object { /** diff --git a/docs/migration/kotlin-fixture-to-some.md b/docs/migration/kotlin-fixture-to-some.md index 2bedc34f..8bc6d72c 100644 --- a/docs/migration/kotlin-fixture-to-some.md +++ b/docs/migration/kotlin-fixture-to-some.md @@ -1,33 +1,31 @@ # Migrating from Appmattus KotlinFixture to Some -This guide explains how to migrate projects from Appmattus `kotlinfixture` to Some's compatibility module `dev.appoutlet:some-kotlin-fixture`. +`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 allows you to replace dependencies and imports while keeping existing `kotlinFixture` configuration and call sites intact. +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. -## 1. Dependency Replacement +## Dependency -Replace the Appmattus `fixture` dependency with `some-kotlin-fixture`: +Replace the Appmattus dependency with `some-kotlin-fixture`: -=== "Gradle (Kotlin DSL)" +=== "Gradle Kotlin DSL" ```kotlin dependencies { // Remove: // implementation("com.appmattus.fixture:fixture:1.2.0") - // Add: implementation("dev.appoutlet:some-kotlin-fixture:0.4.0") } ``` -=== "Gradle (Groovy DSL)" +=== "Gradle Groovy DSL" ```groovy dependencies { // Remove: // implementation 'com.appmattus.fixture:fixture:1.2.0' - // Add: implementation 'dev.appoutlet:some-kotlin-fixture:0.4.0' } ``` @@ -42,28 +40,30 @@ Replace the Appmattus `fixture` dependency with `some-kotlin-fixture`: ``` -## 2. Import Replacement +## Imports -Replace the Appmattus imports with explicit imports for the compatibility symbols you actually use: +Replace the Appmattus import with the compatibility entry point and sequence strategy: ```kotlin // Remove: import com.appmattus.kotlinfixture.kotlinFixture -// Add (import only what you use): -import dev.appoutlet.some.compat.kotlinfixture.Fixture -import dev.appoutlet.some.compat.kotlinfixture.NullabilityStrategy -import dev.appoutlet.some.compat.kotlinfixture.OptionalStrategy -import dev.appoutlet.some.compat.kotlinfixture.RecursionStrategy +// Add: import dev.appoutlet.some.compat.kotlinfixture.SequenceStrategy -import dev.appoutlet.some.compat.kotlinfixture.fixture import dev.appoutlet.some.compat.kotlinfixture.kotlinFixture -import dev.appoutlet.some.compat.kotlinfixture.range ``` -## 3. Supported Features and API Usage +Some configuration types come from the core module: -### Creating a Fixture +```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() @@ -73,108 +73,121 @@ val age: Int = fixture(18..65) val name: String = fixture(listOf("Alice", "Bob", "Charlie")) ``` -### Derived Fixtures (`Fixture.new`) +An empty range falls back to generated data. -Derived fixtures inherit parent configuration without mutating the parent instance: +## Configuration -```kotlin -val baseFixture = kotlinFixture { - factory { "base" } -} +The configuration lambda is a `SomeConfigBuilder` lambda. Type factories receive a `KClass` explicitly: -val derivedFixture = baseFixture.new { - factory { 42 } +```kotlin +val fixture = kotlinFixture { + factory(String::class) { "fixed-value" } + factory(User::class) { + User( + id = random.nextInt(1, 101), + name = "generated-by-factory" + ) + } } ``` -### Sequences (`SequenceStrategy`) +Factory lambdas receive a `FixtureContext`. It exposes `random`, `resolutionStack`, and `strategyProvider`. -Generate sequences lazily: +Property factories use a property reference and override constructor property values: ```kotlin -// Unbounded sequence -val infiniteStream = fixture.asSequence(SequenceStrategy.Unbounded) - -// Bounded sequence -val batch = fixture.asSequence(SequenceStrategy.Bounded(10)).toList() +val fixture = kotlinFixture { + property(User::name) { "OverriddenName" } + property(User::age) { 30 } +} ``` -### Type Factories +## Per-Call Overrides -Override type resolution by exact `KType`: +Pass a builder lambda to an individual fixture invocation. The override applies only to that call: ```kotlin val fixture = kotlinFixture { - factory { "fixed-value" } - factory { User(id = range(1..100), name = fixture()) } + factory(String::class) { "default" } } -``` -### Property Factories +val overridden: String = fixture { + factory(String::class) { "override" } +} -Override constructor parameter values: +val unchanged: String = fixture() +``` + +`Fixture.create` also accepts a one-off builder lambda: ```kotlin -val fixture = kotlinFixture { - property(User::name) { "OverriddenName" } - property(User::age) { range(18..30) } +val value: Int = fixture.create { + factory(Int::class) { 42 } } ``` -### Subtype Mapping +## New Fixtures -Map superclasses or interfaces to concrete implementations: +`Fixture.new` creates a new fixture from the supplied builder lambda: ```kotlin -val fixture = kotlinFixture { - subType() - subType() +val base = kotlinFixture { + factory(String::class) { "base" } +} + +val other = base.new { + factory(Int::class) { 42 } } ``` -### Filters +The new fixture uses the configuration passed to `new`; it does not merge the original fixture's configuration. -Apply predicates and distinct rules to generated values: +## Sequences -```kotlin -val fixture = kotlinFixture { - filter { - filter { it % 2 == 0 } - } +Use `SequenceStrategy.Bounded` for a finite sequence or `SequenceStrategy.Unbounded` for a lazy infinite sequence: - filter { - distinct() - } -} +```kotlin +val batch = fixture + .asSequence(SequenceStrategy.Bounded(10)) + .toList() + +val infinite = fixture + .asSequence(SequenceStrategy.Unbounded) + .take(10) + .toList() ``` -### Configured Random Source +`SequenceStrategy.Bounded(0)` produces an empty sequence. Negative bounds are rejected. -Use a deterministic seed for reproducible data: +## Seeds and Strategies + +Use `seed` for reproducible generation: ```kotlin val fixture = kotlinFixture { - random = Random(12345L) + seed = 12345L } ``` -### Strategy Aliases - -Configure nullability, default constructor parameters, and recursion behavior: +Register Some strategies directly with `strategy`: ```kotlin val fixture = kotlinFixture { - nullabilityStrategy = NullabilityStrategy.NeverNullStrategy - optionalStrategy = OptionalStrategy.AlwaysOptionalStrategy - recursionStrategy = RecursionStrategy.NullRecursionStrategy + strategy(StringStrategy.Readable) + strategy(NullableStrategy.NeverNull) + strategy(DefaultValueStrategy.Generate) } ``` -## 4. Known Compatibility Limitations and Deferred Scope +## Unsupported KotlinFixture APIs + +The simplified compatibility module does not provide the former KotlinFixture-specific APIs: -The following features are currently deferred or unsupported in this phase: +- `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 -- **Collection size strategies** (`CollectionStrategy` and `repeatCount`) are deferred to future releases. -- **Appmattus ecosystem plugins** (`fixture-javafaker`, `fixture-generex`, `fixture-kotest`, and Android compatibility modules) are not included in this compatibility module. -- **Deprecated Appmattus `create(...)` APIs** are omitted; use `fixture()` or `fixture(range)` instead. -- **Property factories** support constructor-declared properties handled by Some's `ClassResolver`. Private non-constructor properties or Java setters will throw a clear exception. +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/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/Fixture.kt b/kotlin-fixture/src/main/kotlin/dev/appoutlet/some/compat/kotlinfixture/Fixture.kt index 244f7992..aa4edc47 100644 --- 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 @@ -3,12 +3,11 @@ package dev.appoutlet.some.compat.kotlinfixture import dev.appoutlet.some.config.SomeConfigBuilder import dev.appoutlet.some.some - /** * Main compatibility generator instance. */ class Fixture @JvmOverloads constructor(val someConfig: SomeConfigBuilder.() -> Unit = {}) { - + inline operator fun invoke( range: Iterable = emptyList(), noinline configuration: SomeConfigBuilder.() -> Unit = {} @@ -34,13 +33,13 @@ class Fixture @JvmOverloads constructor(val someConfig: SomeConfigBuilder.() -> ): Sequence { return sequence { val value = { - some { + some { this.apply(someConfig) this.apply(configuration) } } - when(sequenceStrategy) { + when (sequenceStrategy) { is SequenceStrategy.Bounded -> { repeat(times = sequenceStrategy.numberOfElements) { yield(value()) @@ -57,7 +56,7 @@ class Fixture @JvmOverloads constructor(val someConfig: SomeConfigBuilder.() -> } @JvmOverloads - inline fun create(noinline configuration: SomeConfigBuilder.() -> Unit = {}): T { + inline fun create(noinline configuration: SomeConfigBuilder.() -> Unit = {}): T { return some(configuration) } diff --git a/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/ConfigurationBuilderTest.kt b/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/ConfigurationBuilderTest.kt deleted file mode 100644 index 13a8208b..00000000 --- a/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/ConfigurationBuilderTest.kt +++ /dev/null @@ -1,46 +0,0 @@ -package dev.appoutlet.some.compat.kotlinfixture - -import kotlin.random.Random -import kotlin.reflect.typeOf -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertNotNull - -class ConfigurationBuilderTest { - - @Test - fun `ConfigurationBuilder initializes default values`() { - val builder = ConfigurationBuilder() - val config = builder.build() - - assertEquals(NullabilityStrategy.NeverNullStrategy, config.nullabilityStrategy) - assertEquals(OptionalStrategy.AlwaysOptionalStrategy, config.optionalStrategy) - assertEquals(RecursionStrategy.NullRecursionStrategy, config.recursionStrategy) - } - - @Test - fun `ConfigurationBuilder stores registrations`() { - val customRandom = Random(42) - val builder = ConfigurationBuilder().apply { - random = customRandom - nullabilityStrategy = NullabilityStrategy.AlwaysNullStrategy - optionalStrategy = OptionalStrategy.NeverOptionalStrategy - recursionStrategy = RecursionStrategy.ThrowingRecursionStrategy - factory { 100 } - property(TestUser::name) { "John" } - subType() - filter { distinct() } - } - - val config = builder.build() - - assertEquals(customRandom, config.random) - assertEquals(NullabilityStrategy.AlwaysNullStrategy, config.nullabilityStrategy) - assertEquals(OptionalStrategy.NeverOptionalStrategy, config.optionalStrategy) - assertEquals(RecursionStrategy.ThrowingRecursionStrategy, config.recursionStrategy) - assertNotNull(config.typeFactories[typeOf()]) - assertNotNull(config.propertyFactories[TestUser::class to "name"]) - assertEquals(Dog::class, config.subTypes[Animal::class]) - assertNotNull(config.filters[typeOf()]) - } -} diff --git a/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/EdgeCasesTest.kt b/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/EdgeCasesTest.kt deleted file mode 100644 index e2a5d3da..00000000 --- a/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/EdgeCasesTest.kt +++ /dev/null @@ -1,88 +0,0 @@ -package dev.appoutlet.some.compat.kotlinfixture - -import kotlin.reflect.typeOf -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFailsWith -import kotlin.test.assertNotNull -import kotlin.test.assertTrue - -class EdgeCasesTest { - - @Test - fun `strategy parameter validation throws on out of range values`() { - assertFailsWith { - NullabilityStrategy.RandomlyNullStrategy(-0.1f) - } - assertFailsWith { - NullabilityStrategy.RandomlyNullStrategy(1.1f) - } - assertFailsWith { - OptionalStrategy.RandomlyOptionalStrategy(-0.1f) - } - assertFailsWith { - OptionalStrategy.RandomlyOptionalStrategy(1.1f) - } - } - - @Test - fun `RandomlyOptionalStrategy generates values`() { - val fixture = kotlinFixture { - optionalStrategy = OptionalStrategy.RandomlyOptionalStrategy(0.5f) - } - val user: TestUser = fixture() - assertNotNull(user) - } - - @Test - fun `ConfigurationBuilder untyped methods store registrations`() { - val builder = ConfigurationBuilder().apply { - factory(typeOf()) { "untyped-factory" } - subType(Animal::class, Dog::class) - filter(typeOf()) { - filter { it > 0 } - } - } - val config = builder.build() - val fixture = Fixture(config) - - assertEquals("untyped-factory", fixture()) - assertTrue(fixture() is Dog) - assertTrue(fixture() > 0) - } - - @Test - fun `un-satisfiable filter throws IllegalStateException after max attempts`() { - val fixture = kotlinFixture { - filter { - filter { false } - } - } - - assertFailsWith { - fixture() - } - } - - @Test - fun `range selection with filter filters items before picking`() { - val fixture = kotlinFixture { - filter { - filter { it % 2 == 0 } - } - } - - val selected: Int = fixture(listOf(1, 3, 4, 5)) - assertEquals(4, selected) - } - - @Test - fun `generic subtype resolution maps type arguments`() { - val fixture = kotlinFixture { - subType(List::class, ArrayList::class) - } - - val list: List = fixture() - assertNotNull(list) - } -} 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/FactoryCompatibilityTest.kt b/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/FactoryCompatibilityTest.kt deleted file mode 100644 index 2970e917..00000000 --- a/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/FactoryCompatibilityTest.kt +++ /dev/null @@ -1,59 +0,0 @@ -package dev.appoutlet.some.compat.kotlinfixture - -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertNotNull -import kotlin.test.assertNull -import kotlin.test.assertTrue - -class FactoryCompatibilityTest { - - @Test - fun `type factory resolves exact generic and nullable types`() { - val fixture = kotlinFixture { - factory { "custom-string" } - factory { null } - factory> { GenericBox(777) } - } - - assertEquals("custom-string", fixture()) - assertNull(fixture()) - assertEquals(GenericBox(777), fixture>()) - } - - @Test - fun `factory supports nested fixture and range calls`() { - val fixture = kotlinFixture { - factory { - TestUser( - id = range(100..200), - name = fixture(), - age = range(listOf(20, 25, 30)) - ) - } - } - - val user: TestUser = fixture() - assertTrue(user.id in 100..200) - assertNotNull(user.name) - assertTrue(user.age in listOf(20, 25, 30)) - } - - @Test - fun `factory avoids recursive self-invocation when resolving nested different types`() { - val fixture = kotlinFixture { - factory { "hello" } - factory { - TestUser( - id = 42, - name = fixture(), - age = 30 - ) - } - } - - val user: TestUser = fixture() - assertEquals(42, user.id) - assertEquals("hello", user.name) - } -} diff --git a/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/FilterCompatibilityTest.kt b/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/FilterCompatibilityTest.kt deleted file mode 100644 index 8365b511..00000000 --- a/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/FilterCompatibilityTest.kt +++ /dev/null @@ -1,36 +0,0 @@ -package dev.appoutlet.some.compat.kotlinfixture - -import kotlin.test.Test -import kotlin.test.assertEquals - -class FilterCompatibilityTest { - - @Test - fun `filter predicate filters direct fixture generation`() { - val fixture = kotlinFixture { - filter { - filter { it % 2 == 0 } - } - } - - repeat(10) { - val val1: Int = fixture() - assertEquals(0, val1 % 2) - } - } - - @Test - fun `distinct filter generates unique values in sequence`() { - val fixture = kotlinFixture { - factory { range(listOf(1, 1, 2, 2, 3, 3)) } - filter { - distinct() - } - } - - val sequence = fixture.asSequence(SequenceStrategy.Bounded(3)).toList() - - assertEquals(3, sequence.size) - assertEquals(sequence.size, sequence.toSet().size) - } -} 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 index 4ae850c9..0d3bead9 100644 --- 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 @@ -2,74 +2,82 @@ package dev.appoutlet.some.compat.kotlinfixture import kotlin.test.Test import kotlin.test.assertEquals -import kotlin.test.assertNotEquals import kotlin.test.assertNotNull import kotlin.test.assertTrue class FixtureTest { @Test - fun `fixture generates primitives and data classes`() { + fun `fixture generates primitive and data class values`() { val fixture = kotlinFixture() - val intVal: Int = fixture() - val stringVal: String = fixture() - val userVal: TestUser = fixture() + val number: Int = fixture() + val text: String = fixture() + val user: TestUser = fixture() - assertNotNull(intVal) - assertNotNull(stringVal) - assertNotNull(userVal) - assertNotNull(userVal.name) + assertNotNull(number) + assertNotNull(text) + assertNotNull(user) + assertNotNull(user.name) } @Test - fun `fixture selects value from non-empty iterable`() { + fun `fixture selects values from a non-empty range`() { val fixture = kotlinFixture() + val values = listOf("Alice", "Bob", "Charlie") - val item: String = fixture(listOf("A", "B", "C")) - assertTrue(item in listOf("A", "B", "C")) + val result: String = fixture(values) - val number: Int = fixture(10..20) - assertTrue(number in 10..20) + assertTrue(result in values) } @Test - fun `fixture falls back to generated value for empty iterable`() { + fun `fixture generates a value when the range is empty`() { val fixture = kotlinFixture() - val item: String = fixture(emptyList()) - assertNotNull(item) - assertTrue(item.isNotBlank()) + val result: String = fixture(emptyList()) + + assertTrue(result.isNotEmpty()) } @Test - fun `fixture invoke with per-call configuration does not mutate parent`() { - val parent = kotlinFixture() + fun `per-call configuration overrides the fixture configuration only for that call`() { + val fixture = kotlinFixture { + factory(String::class) { "default" } + } - val overridden: Int = parent { - factory { 999 } + val overridden: String = fixture { + factory(String::class) { "override" } } - assertEquals(999, overridden) - val original: Int = parent() - assertNotEquals(999, original) + assertEquals("override", overridden) + assertEquals("default", fixture()) } @Test - fun `new creates derived fixture preserving configuration`() { - val base = kotlinFixture { - factory { "base-string" } + fun `new creates a fixture using the supplied configuration`() { + val fixture = kotlinFixture { + factory(String::class) { "parent" } + } + + val newFixture = fixture.new { + factory(Int::class) { 42 } } - val derived = base.new { - factory { 123 } + assertEquals(42, newFixture()) + assertEquals("parent", fixture()) + } + + @Test + fun `create generates a value with one-off configuration`() { + val fixture = kotlinFixture { + factory(String::class) { "fixture" } } - assertEquals("base-string", derived()) - assertEquals(123, derived()) + val result: Int = fixture.create { + factory(Int::class) { 42 } + } - // Base remains unchanged for Int - assertEquals("base-string", base()) - assertNotEquals(123, base()) + assertEquals(42, result) } } diff --git a/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/MigrationApiCompilationTest.kt b/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/MigrationApiCompilationTest.kt deleted file mode 100644 index fc2b75ee..00000000 --- a/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/MigrationApiCompilationTest.kt +++ /dev/null @@ -1,30 +0,0 @@ -package dev.appoutlet.some.compat.kotlinfixture - -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertNotNull - -class MigrationApiCompilationTest { - - @Test - fun `migrated code using wildcard import compiles and executes correctly`() { - val fixture: Fixture = kotlinFixture { - nullabilityStrategy = NullabilityStrategy.NeverNullStrategy - optionalStrategy = OptionalStrategy.AlwaysOptionalStrategy - recursionStrategy = RecursionStrategy.NullRecursionStrategy - - factory { "migrated" } - property(TestUser::id) { range(1..100) } - subType() - filter { filter { it > 0 } } - } - - val user: TestUser = fixture() - val animal: Animal = fixture() - val sequence: List = fixture.asSequence(SequenceStrategy.Bounded(3)).toList() - - assertEquals("migrated", user.name) - assertNotNull(animal) - assertEquals(3, sequence.size) - } -} diff --git a/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/NullableStrategyCompatibilityTest.kt b/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/NullableStrategyCompatibilityTest.kt deleted file mode 100644 index de88ffc0..00000000 --- a/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/NullableStrategyCompatibilityTest.kt +++ /dev/null @@ -1,47 +0,0 @@ -package dev.appoutlet.some.compat.kotlinfixture - -import kotlin.test.Test -import kotlin.test.assertNotNull -import kotlin.test.assertNull -import kotlin.test.assertTrue - -class NullableStrategyCompatibilityTest { - - @Test - fun `NeverNullStrategy produces non-null values for nullable types`() { - val fixture = kotlinFixture { - nullabilityStrategy = NullabilityStrategy.NeverNullStrategy - } - - repeat(20) { - val value: String? = fixture() - assertNotNull(value) - } - } - - @Test - fun `AlwaysNullStrategy produces null values for nullable types`() { - val fixture = kotlinFixture { - nullabilityStrategy = NullabilityStrategy.AlwaysNullStrategy - } - - repeat(20) { - val value: String? = fixture() - assertNull(value) - } - } - - @Test - fun `RandomlyNullStrategy produces mix of null and non-null values`() { - val fixture = kotlinFixture { - nullabilityStrategy = NullabilityStrategy.RandomlyNullStrategy(0.5f) - } - - val results = List(50) { fixture() } - val nullCount = results.count { it == null } - val nonNullCount = results.count { it != null } - - assertTrue(nullCount > 0, "Expected at least one null value") - assertTrue(nonNullCount > 0, "Expected at least one non-null value") - } -} diff --git a/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/OptionalStrategyCompatibilityTest.kt b/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/OptionalStrategyCompatibilityTest.kt deleted file mode 100644 index dd7ec06f..00000000 --- a/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/OptionalStrategyCompatibilityTest.kt +++ /dev/null @@ -1,28 +0,0 @@ -package dev.appoutlet.some.compat.kotlinfixture - -import kotlin.test.Test -import kotlin.test.assertEquals - -class OptionalStrategyCompatibilityTest { - - @Test - fun `AlwaysOptionalStrategy uses default constructor values`() { - val fixture = kotlinFixture { - optionalStrategy = OptionalStrategy.AlwaysOptionalStrategy - } - - val user: TestUser = fixture() - assertEquals(30, user.age) - } - - @Test - fun `NeverOptionalStrategy generates values for optional parameters`() { - val fixture = kotlinFixture { - optionalStrategy = OptionalStrategy.NeverOptionalStrategy - factory { 999 } - } - - val user: TestUser = fixture() - assertEquals(999, user.age) - } -} diff --git a/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/PropertyCompatibilityTest.kt b/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/PropertyCompatibilityTest.kt deleted file mode 100644 index b2772d7e..00000000 --- a/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/PropertyCompatibilityTest.kt +++ /dev/null @@ -1,30 +0,0 @@ -package dev.appoutlet.some.compat.kotlinfixture - -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertTrue - -class PropertyCompatibilityTest { - - @Test - fun `property factory overrides parameter value`() { - val fixture = kotlinFixture { - property(TestUser::name) { "OverriddenName" } - property(TestUser::age) { range(50..60) } - } - - val user: TestUser = fixture() - assertEquals("OverriddenName", user.name) - assertTrue(user.age in 50..60) - } - - @Test - fun `property factory supports nested fixture call`() { - val fixture = kotlinFixture { - property(TestUser::name) { fixture().uppercase() } - } - - val user: TestUser = fixture() - assertEquals(user.name, user.name.uppercase()) - } -} diff --git a/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/RandomCompatibilityTest.kt b/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/RandomCompatibilityTest.kt deleted file mode 100644 index 3669960f..00000000 --- a/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/RandomCompatibilityTest.kt +++ /dev/null @@ -1,56 +0,0 @@ -package dev.appoutlet.some.compat.kotlinfixture - -import kotlin.random.Random -import kotlin.test.Test -import kotlin.test.assertEquals - -class RandomCompatibilityTest { - - @Test - fun `deterministic seed produces identical fixture sequence`() { - val seed = 12345L - - val fixture1 = kotlinFixture { - random = Random(seed) - } - - val fixture2 = kotlinFixture { - random = Random(seed) - } - - val list1 = List(5) { fixture1() } - val list2 = List(5) { fixture2() } - - assertEquals(list1, list2) - } - - @Test - fun `configured random is preserved in range and factories`() { - val seed = 9999L - - val fixture1 = kotlinFixture { - random = Random(seed) - factory { - TestUser( - id = range(1..1000), - name = fixture() - ) - } - } - - val fixture2 = kotlinFixture { - random = Random(seed) - factory { - TestUser( - id = range(1..1000), - name = fixture() - ) - } - } - - val user1: TestUser = fixture1() - val user2: TestUser = fixture2() - - assertEquals(user1, user2) - } -} diff --git a/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/RecursionStrategyCompatibilityTest.kt b/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/RecursionStrategyCompatibilityTest.kt deleted file mode 100644 index a91e6e19..00000000 --- a/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/RecursionStrategyCompatibilityTest.kt +++ /dev/null @@ -1,41 +0,0 @@ -package dev.appoutlet.some.compat.kotlinfixture - -import dev.appoutlet.some.exception.SomeCircularReferenceException -import kotlin.test.Test -import kotlin.test.assertFailsWith -import kotlin.test.assertNull - -class RecursionStrategyCompatibilityTest { - - @Test - fun `NullRecursionStrategy resolves circular reference as null for nullable field`() { - val fixture = kotlinFixture { - recursionStrategy = RecursionStrategy.NullRecursionStrategy - } - - val node: RecursiveNode = fixture() - assertNull(node.next) - } - - @Test - fun `ThrowingRecursionStrategy throws exception on circular reference`() { - val fixture = kotlinFixture { - recursionStrategy = RecursionStrategy.ThrowingRecursionStrategy - } - - assertFailsWith { - fixture() - } - } - - @Test - fun `UnresolvedRecursionStrategy throws exception on circular reference`() { - val fixture = kotlinFixture { - recursionStrategy = RecursionStrategy.UnresolvedRecursionStrategy - } - - assertFailsWith { - fixture() - } - } -} 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 index 59506d7b..f642230d 100644 --- 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 @@ -3,55 +3,44 @@ package dev.appoutlet.some.compat.kotlinfixture import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith -import kotlin.test.assertTrue class SequenceStrategyTest { @Test - fun `Unbounded strategy creates infinite sequence`() { - val fixture = kotlinFixture() + fun `unbounded sequence remains lazy`() { + val fixture = kotlinFixture { + factory(Int::class) { 7 } + } - val items = fixture.asSequence(SequenceStrategy.Unbounded).take(10).toList() + val values = fixture.asSequence(SequenceStrategy.Unbounded).take(5).toList() - assertEquals(10, items.size) + assertEquals(listOf(7, 7, 7, 7, 7), values) } @Test - fun `Bounded 0 produces empty sequence`() { - val fixture = kotlinFixture() + fun `bounded sequence emits the requested number of values`() { + val fixture = kotlinFixture { + factory(String::class) { "value" } + } - val items = fixture.asSequence(SequenceStrategy.Bounded(0)).toList() + val values = fixture.asSequence(SequenceStrategy.Bounded(3)).toList() - assertTrue(items.isEmpty()) + assertEquals(listOf("value", "value", "value"), values) } @Test - fun `Bounded N produces exactly N elements`() { - val fixture = kotlinFixture() - - val items = fixture.asSequence(SequenceStrategy.Bounded(5)).toList() + fun `bounded sequence with zero elements is empty`() { + val values = kotlinFixture() + .asSequence(SequenceStrategy.Bounded(0)) + .toList() - assertEquals(5, items.size) + assertEquals(emptyList(), values) } @Test - fun `Bounded throws for negative element count`() { + fun `bounded sequence rejects negative element counts`() { assertFailsWith { SequenceStrategy.Bounded(-1) } } - - @Test - fun `filters are applied before bounded limit`() { - val fixture = kotlinFixture { - filter { - filter { it % 2 == 0 } - } - } - - val items = fixture.asSequence(SequenceStrategy.Bounded(5)).toList() - - assertEquals(5, items.size) - assertTrue(items.all { it % 2 == 0 }) - } } diff --git a/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/SubtypeCompatibilityTest.kt b/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/SubtypeCompatibilityTest.kt deleted file mode 100644 index 81ac6cc6..00000000 --- a/kotlin-fixture/src/test/kotlin/dev/appoutlet/some/compat/kotlinfixture/SubtypeCompatibilityTest.kt +++ /dev/null @@ -1,41 +0,0 @@ -package dev.appoutlet.some.compat.kotlinfixture - -import kotlin.test.Test -import kotlin.test.assertIs - -class SubtypeCompatibilityTest { - - @Test - fun `subType maps superclass or interface to concrete subtype`() { - val fixture = kotlinFixture { - subType() - } - - val animal: Animal = fixture() - assertIs(animal) - } - - @Test - fun `subType preserves derived configuration overrides`() { - val base = kotlinFixture { - subType() - } - - val derived = base.new { - subType() - } - - assertIs(base()) - assertIs(derived()) - } - - @Test - fun `subType mapping Number to Int generates Int instance`() { - val fixture = kotlinFixture { - subType() - } - - val number: Number = fixture() - assertIs(number) - } -} 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 index 12890127..4a994951 100644 --- 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 @@ -6,20 +6,4 @@ internal data class TestUser( val age: Int = 30 ) -internal interface Animal -internal data class Dog(val name: String = "Buddy") : Animal -internal data class Cat(val name: String = "Whiskers") : Animal - -internal data class GenericBox( - val value: T -) - -internal data class RecursiveNode( - val value: Int, - val next: RecursiveNode? -) - -internal data class NonNullableRecursiveNode( - val value: Int, - val next: NonNullableRecursiveNode -) +internal data class GenericBox(val value: T) From b0bc803a249d1f4801800aea92b8a69ca3593a48 Mon Sep 17 00:00:00 2001 From: Messias Junior Date: Fri, 18 Sep 2026 17:45:53 +0100 Subject: [PATCH 10/10] add docs --- .../some/config/CollectionStrategy.kt | 13 +++++ .../some/config/DefaultValueStrategy.kt | 1 + .../some/config/CollectionStrategyTest.kt | 17 ++++-- .../some/compat/kotlinfixture/Fixture.kt | 52 +++++++++++++++++-- .../compat/kotlinfixture/SequenceStrategy.kt | 8 +-- 5 files changed, 79 insertions(+), 12 deletions(-) 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 0b5d4b16..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 { @@ -14,6 +22,11 @@ data class CollectionStrategy( 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 { 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 0e2c9482..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 * 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/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 index aa4edc47..8dde97b0 100644 --- 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 @@ -4,10 +4,26 @@ import dev.appoutlet.some.config.SomeConfigBuilder import dev.appoutlet.some.some /** - * Main compatibility generator instance. + * 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 = {} @@ -24,8 +40,15 @@ class Fixture @JvmOverloads constructor(val someConfig: SomeConfigBuilder.() -> } /** - * Create a sequence of instances of [T] with the option to override any [configuration] for all instances - * generated by this sequence. + * 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, @@ -55,15 +78,38 @@ class Fixture @JvmOverloads constructor(val someConfig: SomeConfigBuilder.() -> } } + /** + * 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 index 8223fb0f..86884c1b 100644 --- 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 @@ -1,18 +1,18 @@ package dev.appoutlet.some.compat.kotlinfixture /** - * Strategy controlling sequence generation length for [Fixture.asSequence]. + * Strategy controlling the length of sequences generated by [Fixture.asSequence]. */ sealed interface SequenceStrategy { /** - * Generates an unbounded sequence of fixture values. + * Generates values until the consumer stops reading the sequence. */ data object Unbounded : SequenceStrategy /** - * Generates a sequence bounded to [numberOfElements]. + * Generates exactly [numberOfElements] values. * - * @property numberOfElements Exact number of elements the sequence should produce. Must be >= 0. + * @property numberOfElements Number of values to generate. Must be non-negative. */ data class Bounded(val numberOfElements: Int) : SequenceStrategy { init {