Skip to content

[Kotlin] @JvmInline value class field loses its set value when a pre-built object is set into a parent via set()/setExp() #1285

Description

@forgreen0624

Describe the bug

When a pre-built object (created via giveMeKotlinBuilder(...).sample()) is assigned to a
parent's property using set() / setExp(), Fixture Monkey decomposes and regenerates that
object. As a result, a @JvmInline value class field loses the value that was fixed on it
— it is regenerated to a random value — while plain (non-value-class) fields of the same object
are preserved.

This is a behavior change: it works as expected on 1.1.15 and breaks starting from 1.1.16
(still reproduces on 1.1.20). It does not appear to be mentioned in the release notes.

Wrapping the value with Values.just(...) works around it, but the change silently breaks
existing tests that relied on the previous "use the instance as-is" behavior.

Your environment

  • Fixture Monkey: 1.1.16 (first broken version; also reproduces on 1.1.20). Works correctly on 1.1.14, 1.1.15. Artifact: fixture-monkey-starter-kotlin
  • Kotlin: 2.1.10
  • Java/JVM target: 17

Steps to reproduce

Run the following Kotlin test. The failure is deterministic (the value class field is always
regenerated), so it reproduces on every run regardless of seed.

import com.navercorp.fixturemonkey.FixtureMonkeyBuilder
import com.navercorp.fixturemonkey.kotlin.KotlinPlugin
import com.navercorp.fixturemonkey.kotlin.giveMeKotlinBuilder
import kotlin.test.assertEquals
import org.junit.Test

class FmValueClassReproTest {

    @JvmInline
    value class Code(val value: String)

    data class Child(val code: Code, val name: String)
    data class Parent(val child: Child)

    private val fm = FixtureMonkeyBuilder().plugin(KotlinPlugin()).build()

    // A) set & assert the VALUE CLASS field
    @Test
    fun valueClassField() {
        val fixedCode = Code("FIXED")
        val child = fm.giveMeKotlinBuilder<Child>()
            .setExp(Child::code, fixedCode)
            .sample()
        assertEquals(fixedCode, child.code) // standalone
        val parent = fm.giveMeKotlinBuilder<Parent>()
            .setExp(Parent::child, child)
            .sample()
        println("Check. parent.child: ${parent.child}, child: $child")
        assertEquals(fixedCode, parent.child.code) // hypothesis: FAIL on 1.1.16
    }

    // B) set & assert a PLAIN field (object still contains a value class field)
    @Test
    fun plainFieldInValueClassObject() {
        val child = fm.giveMeKotlinBuilder<Child>().setExp(Child::name, "FIXED").sample()
        val parent = fm.giveMeKotlinBuilder<Parent>().setExp(Parent::child, child).sample()
        println("DIAG name=${parent.child.name}")
        assertEquals("FIXED", parent.child.name)
    }
}
Image Image

Expected behaviour

parent.child.code should remain Code("FIXED") — the value fixed on the injected instance's
value class field should be preserved when the object is set into a parent (as it was in 1.1.15,
and as plain fields still are in 1.1.16+).

Actual behaviour

parent.child.code is regenerated to a random Code(...), so the assertion fails. Only the
@JvmInline value class field is affected; plain fields (e.g. name) of the same object are
preserved. Wrapping with Values.just(child) restores the correct value.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions