From a0dfe36285cd9b5491cdd2e5091f75601ad37519 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 18 Jun 2026 18:20:19 +0000 Subject: [PATCH] Add FloatStrategy to control generated Float value range - Introduce FloatStrategy in dev.appoutlet.some.config to allow constraining generated Float values via a ClosedFloatingPointRange or a fixed value. - Update FloatResolver to consume the active FloatStrategy from StrategyProvider. - Register FloatStrategy in SomeConfig.defaultStrategies() and update wiring in SomeConfig.buildResolvers(). - Update KDoc in Strategy.kt and SomeConfigBuilder.kt with FloatStrategy details and examples. - Add comprehensive unit tests for FloatStrategy and updated tests for FloatResolver. Co-authored-by: MessiasLima <10220064+MessiasLima@users.noreply.github.com> --- .../appoutlet/some/config/FloatStrategy.kt | 28 ++++++++++++++ .../dev/appoutlet/some/config/SomeConfig.kt | 17 ++++++++- .../some/config/SomeConfigBuilder.kt | 1 + .../dev/appoutlet/some/config/Strategy.kt | 2 +- .../appoutlet/some/resolver/FloatResolver.kt | 21 ++++++++++- .../some/config/FloatStrategyTest.kt | 32 ++++++++++++++++ .../some/resolver/FloatResolverTest.kt | 37 ++++++++++++++++--- 7 files changed, 127 insertions(+), 11 deletions(-) create mode 100644 core/src/main/kotlin/dev/appoutlet/some/config/FloatStrategy.kt create mode 100644 core/src/test/kotlin/dev/appoutlet/some/config/FloatStrategyTest.kt diff --git a/core/src/main/kotlin/dev/appoutlet/some/config/FloatStrategy.kt b/core/src/main/kotlin/dev/appoutlet/some/config/FloatStrategy.kt new file mode 100644 index 00000000..64fb95ce --- /dev/null +++ b/core/src/main/kotlin/dev/appoutlet/some/config/FloatStrategy.kt @@ -0,0 +1,28 @@ +package dev.appoutlet.some.config + +/** + * Strategy for generating float values. + * + * @property range The range within which float values will be generated (default 0.0f..1.0f). + */ +data class FloatStrategy( + val range: ClosedFloatingPointRange = 0.0f..1.0f +) : Strategy { + override val key = FloatStrategy::class + + init { + require(range.start <= range.endInclusive) { "range.start must be less than or equal to range.endInclusive" } + } + + /** + * Convenience constructor for a fixed float value. + */ + constructor(fixed: Float) : this(fixed..fixed) + + companion object { + /** + * The default float strategy. + */ + val default: FloatStrategy get() = FloatStrategy() + } +} diff --git a/core/src/main/kotlin/dev/appoutlet/some/config/SomeConfig.kt b/core/src/main/kotlin/dev/appoutlet/some/config/SomeConfig.kt index 7d7c429e..8fba142e 100644 --- a/core/src/main/kotlin/dev/appoutlet/some/config/SomeConfig.kt +++ b/core/src/main/kotlin/dev/appoutlet/some/config/SomeConfig.kt @@ -55,7 +55,7 @@ import kotlin.reflect.KClass * @param propertyFactories Custom property factories keyed by class and property name. */ data class SomeConfig( - val strategies: Map, Strategy> = emptyMap(), + val strategies: Map, Strategy> = defaultStrategies(), val seed: Long? = null, val typeFactories: Map, FixtureContext.() -> Any?> = emptyMap(), val propertyFactories: Map, String>, FixtureContext.() -> Any?> = emptyMap(), @@ -116,7 +116,7 @@ data class SomeConfig( IntResolver(random), LongResolver(random), DoubleResolver(random), - FloatResolver(random), + FloatResolver(strategyProvider, random), BooleanResolver(random), CharResolver(random), ByteResolver(random), @@ -194,4 +194,17 @@ data class SomeConfig( * @return [Random] seeded with [seed] if set, or [Random.Default] otherwise. */ internal fun buildRandom(): Random = seed?.let { Random(it) } ?: Random.Default + + companion object { + /** + * Returns the default strategies for fixture generation. + */ + fun defaultStrategies(): Map, Strategy> = mapOf( + NullableStrategy::class to NullableStrategy.default, + StringStrategy::class to StringStrategy.default, + CollectionStrategy::class to CollectionStrategy.default, + FloatStrategy::class to FloatStrategy.default, + DefaultValueStrategy::class to DefaultValueStrategy.default, + ) + } } diff --git a/core/src/main/kotlin/dev/appoutlet/some/config/SomeConfigBuilder.kt b/core/src/main/kotlin/dev/appoutlet/some/config/SomeConfigBuilder.kt index 0f284038..ad198617 100644 --- a/core/src/main/kotlin/dev/appoutlet/some/config/SomeConfigBuilder.kt +++ b/core/src/main/kotlin/dev/appoutlet/some/config/SomeConfigBuilder.kt @@ -21,6 +21,7 @@ import kotlin.reflect.full.instanceParameter * strategy(NullableStrategy.NeverNull) * strategy(StringStrategy.Uuid) * strategy(CollectionStrategy(5..10)) + * strategy(FloatStrategy(0.0f..100.0f)) * } * ``` * diff --git a/core/src/main/kotlin/dev/appoutlet/some/config/Strategy.kt b/core/src/main/kotlin/dev/appoutlet/some/config/Strategy.kt index 629cd044..57365a72 100644 --- a/core/src/main/kotlin/dev/appoutlet/some/config/Strategy.kt +++ b/core/src/main/kotlin/dev/appoutlet/some/config/Strategy.kt @@ -6,7 +6,7 @@ import kotlin.reflect.KClass * Marker interface for strategy objects that configure fixture generation behavior. * * All built-in strategies ([NullableStrategy], [StringStrategy], [CollectionStrategy], - * and [DefaultValueStrategy]) implement this interface. Custom strategies can also be + * [FloatStrategy], and [DefaultValueStrategy]) implement this interface. Custom strategies can also be * created by implementing [Strategy] and registering them via [SomeConfigBuilder.strategy]. * * The [key] property determines the registration bucket for the strategy. For sealed interface diff --git a/core/src/main/kotlin/dev/appoutlet/some/resolver/FloatResolver.kt b/core/src/main/kotlin/dev/appoutlet/some/resolver/FloatResolver.kt index 1b830593..c30f1e5d 100644 --- a/core/src/main/kotlin/dev/appoutlet/some/resolver/FloatResolver.kt +++ b/core/src/main/kotlin/dev/appoutlet/some/resolver/FloatResolver.kt @@ -1,15 +1,32 @@ package dev.appoutlet.some.resolver +import dev.appoutlet.some.config.FloatStrategy import dev.appoutlet.some.core.ResolverChain +import dev.appoutlet.some.core.StrategyProvider import dev.appoutlet.some.core.TypeResolver +import dev.appoutlet.some.core.get import kotlin.random.Random import kotlin.reflect.KType import kotlin.reflect.typeOf -class FloatResolver(val random: Random) : TypeResolver { +/** + * Resolves [Float] types using the active [FloatStrategy]. + * + * @param strategyProvider Provider of all configured generation strategies. + * @param random Random source used for generating float values. + */ +class FloatResolver( + strategyProvider: StrategyProvider, + private val random: Random +) : TypeResolver { + private val floatStrategy = strategyProvider.get() ?: FloatStrategy.default + override fun canResolve(type: KType): Boolean = type == typeOf() override fun resolve(type: KType, chain: ResolverChain): Any { - return random.nextFloat() + val range = floatStrategy.range + if (range.start == range.endInclusive) return range.start + + return random.nextDouble(range.start.toDouble(), range.endInclusive.toDouble()).toFloat() } } diff --git a/core/src/test/kotlin/dev/appoutlet/some/config/FloatStrategyTest.kt b/core/src/test/kotlin/dev/appoutlet/some/config/FloatStrategyTest.kt new file mode 100644 index 00000000..63d5a75a --- /dev/null +++ b/core/src/test/kotlin/dev/appoutlet/some/config/FloatStrategyTest.kt @@ -0,0 +1,32 @@ +package dev.appoutlet.some.config + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +class FloatStrategyTest { + @Test + fun `default strategy uses range 0 to 1`() { + val strategy = FloatStrategy.default + assertEquals(0.0f..1.0f, strategy.range) + } + + @Test + fun `secondary constructor pins value`() { + val strategy = FloatStrategy(5.0f) + assertEquals(5.0f..5.0f, strategy.range) + } + + @Test + fun `validation rejects inverted range`() { + assertFailsWith { + FloatStrategy(5.0f..2.0f) + } + } + + @Test + fun `zero-width range is allowed`() { + val strategy = FloatStrategy(2.0f..2.0f) + assertEquals(2.0f..2.0f, strategy.range) + } +} diff --git a/core/src/test/kotlin/dev/appoutlet/some/resolver/FloatResolverTest.kt b/core/src/test/kotlin/dev/appoutlet/some/resolver/FloatResolverTest.kt index 2f9ef15a..c7cc852b 100644 --- a/core/src/test/kotlin/dev/appoutlet/some/resolver/FloatResolverTest.kt +++ b/core/src/test/kotlin/dev/appoutlet/some/resolver/FloatResolverTest.kt @@ -1,5 +1,7 @@ package dev.appoutlet.some.resolver +import dev.appoutlet.some.config.FloatStrategy +import dev.appoutlet.some.config.SomeConfig import dev.appoutlet.some.test.defaultTestChain import kotlin.random.Random import kotlin.reflect.typeOf @@ -7,35 +9,58 @@ import kotlin.test.Test import kotlin.test.assertFalse import kotlin.test.assertIs import kotlin.test.assertTrue +import kotlin.test.assertEquals class FloatResolverTest { @Test fun `FloatResolver generates float values`() { - val resolver = FloatResolver(Random.Default) + val resolver = FloatResolver(SomeConfig(), Random.Default) val result = resolver.resolve(typeOf(), defaultTestChain) assertIs(result) } @Test - fun `FloatResolver generates values between 0 and 1`() { - val resolver = FloatResolver(Random.Default) + fun `FloatResolver generates values between 0 and 1 by default`() { + val resolver = FloatResolver(SomeConfig(), Random.Default) repeat(100) { val result = resolver.resolve(typeOf(), defaultTestChain) as Float - assertTrue(result in 0.0f..<1.0f, "Expected value between 0.0 and 1.0, got $result") + assertTrue(result in 0.0f..1.0f, "Expected value between 0.0 and 1.0, got $result") + } + } + + @Test + fun `FloatResolver respects FloatStrategy range`() { + val config = SomeConfig(strategies = mapOf(FloatStrategy::class to FloatStrategy(10.0f..20.0f))) + val resolver = FloatResolver(config, Random.Default) + + repeat(100) { + val result = resolver.resolve(typeOf(), defaultTestChain) as Float + assertTrue(result in 10.0f..20.0f, "Expected value between 10.0 and 20.0, got $result") + } + } + + @Test + fun `FloatResolver handles zero-width range`() { + val config = SomeConfig(strategies = mapOf(FloatStrategy::class to FloatStrategy(5.0f))) + val resolver = FloatResolver(config, Random.Default) + + repeat(10) { + val result = resolver.resolve(typeOf(), defaultTestChain) as Float + assertEquals(5.0f, result) } } @Test fun `FloatResolver canResolve detects Float type`() { - val resolver = FloatResolver(Random.Default) + val resolver = FloatResolver(SomeConfig(), Random.Default) assertTrue(resolver.canResolve(typeOf())) } @Test fun `FloatResolver rejects non-Float types`() { - val resolver = FloatResolver(Random.Default) + val resolver = FloatResolver(SomeConfig(), Random.Default) assertFalse(resolver.canResolve(typeOf())) assertFalse(resolver.canResolve(typeOf())) assertFalse(resolver.canResolve(typeOf()))