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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions core/src/main/kotlin/dev/appoutlet/some/config/FloatStrategy.kt
Original file line number Diff line number Diff line change
@@ -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<Float> = 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" }
}
Comment on lines +13 to +15

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

It is a good practice to ensure that the range bounds are finite (Float.isFinite()). If a user passes Float.NEGATIVE_INFINITY or Float.POSITIVE_INFINITY, generating a random value in FloatResolver using Random.nextDouble will fail or produce unexpected results (like NaN or Infinity) because uniform sampling over an infinite range is not supported.

Suggested change
init {
require(range.start <= range.endInclusive) { "range.start must be less than or equal to range.endInclusive" }
}
init {
require(range.start.isFinite() && range.endInclusive.isFinite()) { "range bounds must be finite" }
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()
}
}
17 changes: 15 additions & 2 deletions core/src/main/kotlin/dev/appoutlet/some/config/SomeConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<KClass<out Strategy>, Strategy> = emptyMap(),
val strategies: Map<KClass<out Strategy>, Strategy> = defaultStrategies(),
Comment on lines 57 to +58

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

By changing the default value of strategies to defaultStrategies(), an inconsistency is introduced between direct instantiation and DSL-based building. Specifically, SomeConfig() will have a populated strategies map, whereas buildSomeConfig { } (which uses SomeConfigBuilder starting with an empty map) will produce a SomeConfig with an empty strategies map.

Since all resolvers (including the new FloatResolver) already correctly fall back to their respective .default strategies when the strategy is missing from the provider, we don't need to pre-populate strategies with defaults. Reverting this to emptyMap() maintains consistency, simplifies the configuration, and avoids redundant map allocations.

Suggested change
data class SomeConfig(
val strategies: Map<KClass<out Strategy>, Strategy> = emptyMap(),
val strategies: Map<KClass<out Strategy>, Strategy> = defaultStrategies(),
data class SomeConfig(
val strategies: Map<KClass<out Strategy>, Strategy> = emptyMap(),

val seed: Long? = null,
val typeFactories: Map<KClass<*>, FixtureContext.() -> Any?> = emptyMap(),
val propertyFactories: Map<Pair<KClass<*>, String>, FixtureContext.() -> Any?> = emptyMap(),
Expand Down Expand Up @@ -116,7 +116,7 @@ data class SomeConfig(
IntResolver(random),
LongResolver(random),
DoubleResolver(random),
FloatResolver(random),
FloatResolver(strategyProvider, random),
BooleanResolver(random),
CharResolver(random),
ByteResolver(random),
Expand Down Expand Up @@ -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<KClass<out Strategy>, 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,
)
}
Comment on lines +198 to +209

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Since we are reverting strategies to default to emptyMap(), the defaultStrategies() helper and the companion object are no longer needed and can be safely removed.

}
Original file line number Diff line number Diff line change
Expand Up @@ -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))
* }
* ```
*
Expand Down
2 changes: 1 addition & 1 deletion core/src/main/kotlin/dev/appoutlet/some/config/Strategy.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 19 additions & 2 deletions core/src/main/kotlin/dev/appoutlet/some/resolver/FloatResolver.kt
Original file line number Diff line number Diff line change
@@ -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>() ?: FloatStrategy.default

override fun canResolve(type: KType): Boolean = type == typeOf<Float>()

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()
}
}
Original file line number Diff line number Diff line change
@@ -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<IllegalArgumentException> {
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)
}
}
Original file line number Diff line number Diff line change
@@ -1,41 +1,66 @@
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
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<Float>(), defaultTestChain)
assertIs<Float>(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<Float>(), 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<Float>(), 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<Float>(), 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<Float>()))
}

@Test
fun `FloatResolver rejects non-Float types`() {
val resolver = FloatResolver(Random.Default)
val resolver = FloatResolver(SomeConfig(), Random.Default)
assertFalse(resolver.canResolve(typeOf<String>()))
assertFalse(resolver.canResolve(typeOf<Int>()))
assertFalse(resolver.canResolve(typeOf<Double>()))
Expand Down
Loading