-
Notifications
You must be signed in to change notification settings - Fork 0
feat: float strategy #89
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
MessiasLima
merged 3 commits into
main
from
69-add-floatstrategy-to-control-generated-float-value-range
Jun 22, 2026
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
22 changes: 22 additions & 0 deletions
22
core/src/main/kotlin/dev/appoutlet/some/config/FloatStrategy.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| package dev.appoutlet.some.config | ||
|
|
||
| data class FloatStrategy( | ||
| val range: ClosedFloatingPointRange<Float> = 0.0f..1.0f | ||
| ) : Strategy { | ||
| override val key = FloatStrategy::class | ||
|
|
||
| constructor(fixed: Float) : this(fixed..fixed) | ||
|
|
||
| init { | ||
| require(range.start <= range.endInclusive) { | ||
| "range.start must be less than or equal to range.endInclusive" | ||
| } | ||
| } | ||
|
|
||
| companion object { | ||
| /** | ||
| * The default float strategy. | ||
| */ | ||
| val default = FloatStrategy() | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
20 changes: 18 additions & 2 deletions
20
core/src/main/kotlin/dev/appoutlet/some/resolver/FloatResolver.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,15 +1,31 @@ | ||
| package dev.appoutlet.some.resolver | ||
|
|
||
| import dev.appoutlet.some.config.FloatStrategy | ||
| import dev.appoutlet.some.core.Resolver | ||
| import dev.appoutlet.some.core.ResolverChain | ||
| import dev.appoutlet.some.core.StrategyProvider | ||
| import dev.appoutlet.some.core.get | ||
| import kotlin.random.Random | ||
| import kotlin.reflect.KType | ||
| import kotlin.reflect.typeOf | ||
|
|
||
| class FloatResolver(val random: Random) : Resolver { | ||
| class FloatResolver( | ||
| strategyProvider: StrategyProvider, | ||
| private val random: Random | ||
| ) : Resolver { | ||
| 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 | ||
| return if (range.start == range.endInclusive) { | ||
| range.start | ||
| } else { | ||
| random.nextDouble( | ||
| from = range.start.toDouble(), | ||
| until = range.endInclusive.toDouble(), | ||
| ).toFloat() | ||
| } | ||
| } | ||
| } | ||
67 changes: 67 additions & 0 deletions
67
core/src/test/kotlin/dev/appoutlet/some/config/FloatStrategyTest.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| package dev.appoutlet.some.config | ||
|
|
||
| import kotlin.test.Test | ||
| import kotlin.test.assertEquals | ||
| import kotlin.test.assertFailsWith | ||
| import kotlin.test.assertSame | ||
|
|
||
| class FloatStrategyTest { | ||
| @Test | ||
| fun `FloatStrategy has default range 0_0 to 1_0`() { | ||
| val strategy = FloatStrategy() | ||
| assertEquals(0.0f, strategy.range.start) | ||
| assertEquals(1.0f, strategy.range.endInclusive) | ||
| } | ||
|
|
||
| @Test | ||
| fun `FloatStrategy default companion is no-arg instance`() { | ||
| val strategy = FloatStrategy.default | ||
| assertEquals(0.0f, strategy.range.start) | ||
| assertEquals(1.0f, strategy.range.endInclusive) | ||
| } | ||
|
|
||
| @Test | ||
| fun `FloatStrategy accepts valid range`() { | ||
| val strategy = FloatStrategy(1.0f..5.0f) | ||
| assertEquals(1.0f, strategy.range.start) | ||
| assertEquals(5.0f, strategy.range.endInclusive) | ||
| } | ||
|
|
||
| @Test | ||
| fun `FloatStrategy accepts zero-width range`() { | ||
| val strategy = FloatStrategy(3.0f..3.0f) | ||
| assertEquals(3.0f, strategy.range.start) | ||
| assertEquals(3.0f, strategy.range.endInclusive) | ||
| } | ||
|
|
||
| @Test | ||
| fun `FloatStrategy accepts negative range`() { | ||
| val strategy = FloatStrategy(-5.0f..-1.0f) | ||
| assertEquals(-5.0f, strategy.range.start) | ||
| assertEquals(-1.0f, strategy.range.endInclusive) | ||
| } | ||
|
|
||
| @Test | ||
| fun `FloatStrategy rejects inverted range`() { | ||
| val exception = assertFailsWith<IllegalArgumentException> { | ||
| FloatStrategy(5.0f..2.0f) | ||
| } | ||
| assertEquals( | ||
| "range.start must be less than or equal to range.endInclusive", | ||
| exception.message | ||
| ) | ||
| } | ||
|
|
||
| @Test | ||
| fun `FloatStrategy fixed constructor is equivalent to zero-width range`() { | ||
| val fixed = FloatStrategy(3.0f) | ||
| val range = FloatStrategy(3.0f..3.0f) | ||
| assertEquals(range.range.start, fixed.range.start) | ||
| assertEquals(range.range.endInclusive, fixed.range.endInclusive) | ||
| } | ||
|
|
||
| @Test | ||
| fun `FloatStrategy key is its own class`() { | ||
| assertSame(FloatStrategy::class, FloatStrategy().key) | ||
| } | ||
| } |
69 changes: 64 additions & 5 deletions
69
core/src/test/kotlin/dev/appoutlet/some/resolver/FloatResolverTest.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,43 +1,102 @@ | ||
| package dev.appoutlet.some.resolver | ||
|
|
||
| import dev.appoutlet.some.config.DefaultStrategyProvider | ||
| import dev.appoutlet.some.config.FloatStrategy | ||
| import dev.appoutlet.some.config.NullableStrategy | ||
| import dev.appoutlet.some.config.buildSomeConfig | ||
| import dev.appoutlet.some.core.ResolverChain | ||
| import dev.appoutlet.some.test.defaultTestChain | ||
| import kotlin.random.Random | ||
| import kotlin.reflect.typeOf | ||
| import kotlin.test.Test | ||
| import kotlin.test.assertEquals | ||
| import kotlin.test.assertFalse | ||
| import kotlin.test.assertIs | ||
| import kotlin.test.assertTrue | ||
|
|
||
| class FloatResolverTest { | ||
| @Test | ||
| fun `FloatResolver generates float values`() { | ||
| val resolver = FloatResolver(Random.Default) | ||
| val resolver = FloatResolver(DefaultStrategyProvider(), 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(DefaultStrategyProvider(), 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") | ||
| } | ||
| } | ||
|
|
||
| @Test | ||
| fun `FloatResolver honors a custom range`() { | ||
| val resolver = FloatResolver( | ||
| DefaultStrategyProvider(mapOf(FloatStrategy::class to FloatStrategy(0.0f..10.0f))), | ||
| Random.Default | ||
| ) | ||
|
|
||
| repeat(100) { | ||
| val result = resolver.resolve(typeOf<Float>(), defaultTestChain) as Float | ||
| assertTrue(result in 0.0f..<10.0f, "Expected value between 0.0 and 10.0, got $result") | ||
| } | ||
| } | ||
|
|
||
| @Test | ||
| fun `FloatResolver honors a negative range`() { | ||
| val resolver = FloatResolver( | ||
| DefaultStrategyProvider(mapOf(FloatStrategy::class to FloatStrategy(-5.0f..-1.0f))), | ||
| Random.Default | ||
| ) | ||
|
|
||
| repeat(100) { | ||
| val result = resolver.resolve(typeOf<Float>(), defaultTestChain) as Float | ||
| assertTrue(result in -5.0f..<-1.0f, "Expected value between -5.0 and -1.0, got $result") | ||
| } | ||
| } | ||
|
|
||
| @Test | ||
| fun `FloatResolver always returns fixed value for pinned strategy`() { | ||
| val resolver = FloatResolver( | ||
| DefaultStrategyProvider(mapOf(FloatStrategy::class to FloatStrategy(5.0f))), | ||
| Random.Default | ||
| ) | ||
|
|
||
| repeat(50) { | ||
| 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(DefaultStrategyProvider(), Random.Default) | ||
| assertTrue(resolver.canResolve(typeOf<Float>())) | ||
| } | ||
|
|
||
| @Test | ||
| fun `FloatResolver rejects non-Float types`() { | ||
| val resolver = FloatResolver(Random.Default) | ||
| val resolver = FloatResolver(DefaultStrategyProvider(), Random.Default) | ||
| assertFalse(resolver.canResolve(typeOf<String>())) | ||
| assertFalse(resolver.canResolve(typeOf<Int>())) | ||
| assertFalse(resolver.canResolve(typeOf<Double>())) | ||
| } | ||
|
|
||
| @Test | ||
| fun `FloatStrategy integrates with SomeConfig`() { | ||
| val config = buildSomeConfig { | ||
| strategy(FloatStrategy(0.0f..10.0f)) | ||
| } | ||
| val resolvers = config.buildResolvers() | ||
| val chain = ResolverChain(resolvers, config[NullableStrategy::class]) | ||
|
|
||
| repeat(50) { | ||
| val result = chain.resolve(typeOf<Float>()) as Float | ||
| assertTrue(result in 0.0f..<10.0f, "Expected value between 0.0 and 10.0, got $result") | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| # Float Strategy | ||
|
|
||
| Controls the range of `Float` values generated during fixture generation. | ||
|
|
||
| ## Configuration | ||
|
|
||
| The `FloatStrategy` takes a single `range` parameter — a `ClosedFloatingPointRange<Float>` specifying the inclusive minimum and inclusive maximum of generated `Float` values. The end of the range is treated as exclusive during generation, matching `random.nextDouble(from, until)` semantics. | ||
|
|
||
| ```kotlin | ||
| // Default: 0.0 to 1.0 (exclusive end) | ||
| some { strategy(FloatStrategy()) } | ||
|
|
||
| // Positive range | ||
| some { strategy(FloatStrategy(0.0f..10.0f)) } | ||
|
|
||
| // Negative range | ||
| some { strategy(FloatStrategy(-5.0f..-1.0f)) } | ||
|
|
||
| // Range that spans zero | ||
| some { strategy(FloatStrategy(-1.0f..1.0f)) } | ||
| ``` | ||
|
|
||
| ## Pinning a fixed value | ||
|
|
||
| A secondary constructor `FloatStrategy(fixed: Float)` is provided as a convenience for pinning generation to a single value. It is equivalent to `FloatStrategy(fixed..fixed)`. | ||
|
|
||
| ```kotlin | ||
| // Pin to a single value | ||
| some { strategy(FloatStrategy(5.0f)) } | ||
|
|
||
| // Equivalent zero-width range | ||
| some { strategy(FloatStrategy(5.0f..5.0f)) } | ||
| ``` | ||
|
|
||
| Zero-width ranges (`start == endInclusive`) are always honored — the resolver returns the fixed value directly without invoking the random source. | ||
|
|
||
| ## Default | ||
|
|
||
| | Property | Default value | | ||
| |----------|---------------| | ||
| | `range` | `0.0f..1.0f` | | ||
|
|
||
| The default range preserves the behavior of `random.nextFloat()` (`[0.0, 1.0)`), so existing users see no change. | ||
|
|
||
| ## Affected types | ||
|
|
||
| The float strategy applies to the `Float` primitive type: | ||
|
|
||
| - `Float` | ||
|
|
||
| ## Validation | ||
|
|
||
| The `FloatStrategy` constructor enforces one constraint on the `range` parameter via a `require()` check. It throws `IllegalArgumentException` on failure. | ||
|
|
||
| ### Start must be less than or equal to end | ||
|
|
||
| `range.start` must be `<=` `range.endInclusive`. Inverted ranges are rejected; equal bounds (zero-width ranges) are allowed. | ||
|
|
||
| ```kotlin | ||
| FloatStrategy(0.0f..1.0f) // OK | ||
| FloatStrategy(5.0f..2.0f) // IllegalArgumentException: range.start must be less than or equal to range.endInclusive | ||
| FloatStrategy(3.0f..3.0f) // OK — zero-width range, always returns 3.0f | ||
| ``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.