Skip to content
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

Unable to generate random values to record type #514

Open
garcia-jj opened this issue Apr 16, 2024 · 2 comments
Open

Unable to generate random values to record type #514

garcia-jj opened this issue Apr 16, 2024 · 2 comments

Comments

@garcia-jj
Copy link

garcia-jj commented Apr 16, 2024

I'm trying to use records with easy-random 5.0.0, but I was unable to run properly, as you can see in the following code.

public record MyRecord(Long b, String a) {

}

And the runner is

final var testing = new EasyRandom().nextObject(MyRecord.class);

And I got this error:

org.jeasy.random.ObjectCreationException: Unable to create a random instance of type class foo.bar.MyRecord


Caused by: java.lang.IllegalAccessException: Can not set final java.lang.Long field foo.bar.MyRecord.a to java.lang.Long
	at java.base/jdk.internal.reflect.UnsafeFieldAccessorImpl.throwFinalFieldIllegalAccessException(UnsafeFieldAccessorImpl.java:76)
	at java.base/jdk.internal.reflect.UnsafeFieldAccessorImpl.throwFinalFieldIllegalAccessException(UnsafeFieldAccessorImpl.java:80)
	at java.base/jdk.internal.reflect.UnsafeQualifiedObjectFieldAccessorImpl.set(UnsafeQualifiedObjectFieldAccessorImpl.java:79)

My Java version

java version "17.0.10" 2024-01-16 LTS
Java(TM) SE Runtime Environment (build 17.0.10+11-LTS-240)
Java HotSpot(TM) 64-Bit Server VM (build 17.0.10+11-LTS-240, mixed mode, sharing)

Thank U

@pcuriel
Copy link

pcuriel commented Apr 26, 2024

Hi! This is not a bug, but a feature that's being implemented:
#397

This project is in maintaince mode, so sadly I think there is no estimation of when the feature will be finally released.

@bduisenov
Copy link

We're using a workaround to generate random data for Java records. The process works by dynamically creating a companion class at runtime, passing it to the randomizer, and then copying the generated values to the record.

import net.bytebuddy.ByteBuddy;
import net.bytebuddy.NamingStrategy;
import net.bytebuddy.description.modifier.Visibility;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.stream.Stream;

public class DefaultEasyRandom extends EasyRandom {

    private static final NamingStrategy.Suffixing namingStrategy = new NamingStrategy.Suffixing("RecordClassAdapter");

    private static final ConcurrentMap<Class<?>, Class<?>> dynamicClassCache = new ConcurrentHashMap<>();

    private final EasyRandomParameters parameters;

    public DefaultEasyRandom(EasyRandomParameters parameters) {
        super(parameters);
        this.parameters = parameters;
    }

    @Override
    public <T> T nextObject(Class<T> type) {
        return doPopulateBean(type, new RandomizationContext(type, parameters));
    }

    @Override
    public <T> Stream<T> objects(Class<T> type, int streamSize) {
        return super.objects(type, streamSize);
    }

    @Override
    <T> T doPopulateBean(Class<T> type, RandomizationContext context) {
        return type.isRecord() ? this.doPopulateRecord(type, context) : super.doPopulateBean(type, context);
    }

    <T> T doPopulateRecord(Class<T> type, RandomizationContext context) {
        try {
            return doPopulateRecordInternal(type, context);
        } catch (Exception e) {
            return sneakyThrow(e);
        }
    }

    private <T> T doPopulateRecordInternal(Class<T> type, RandomizationContext context) throws Exception {
        Class<T> targetType = getDynamicRecordClassAdapter(type);

        final var result = super.doPopulateBean(targetType, context);

        final var sourceFields = getFields(targetType);
        final var targetParams = new Object[sourceFields.size()];
        for (int i = 0; i < sourceFields.size(); i++) {
            final var field = sourceFields.get(i);
            targetParams[i] = field.get(result);
        }

        return newInstance(type, targetParams);
    }

    @SuppressWarnings("unchecked")
    private <T> Class<T> getDynamicRecordClassAdapter(Class<T> targetType) {
        return (Class<T>) dynamicClassCache.computeIfAbsent(targetType, this::defineDynamicRecordClassAdapter);
    }

    private Class<?> defineDynamicRecordClassAdapter(Class<?> targetType) {
        if (!targetType.isRecord()) {
            throw new IllegalArgumentException("Only records are supported");
        }

        final var fields = getFields(targetType);

        var definitionBuilder = new ByteBuddy().with(namingStrategy).subclass(Object.class);

        for (Field field : fields) {
            definitionBuilder = definitionBuilder.defineField(field.getName(), field.getType(), Visibility.PRIVATE);
        }

        try (final var dynamicType = definitionBuilder.make()) {
            final var loadedType = dynamicType.load(targetType.getClassLoader());

            return loadedType.getLoaded();
        } catch (Exception e) {
            return sneakyThrow(e);
        }
    }

    private static <A> List<Field> getFields(Class<A> targetType) {
        final var declaredFields = targetType.getDeclaredFields();
        final var result = new ArrayList<Field>(declaredFields.length);

        for (var field : declaredFields) {
            if (!Modifier.isStatic(field.getModifiers())) {
                field.setAccessible(true);
                result.add(field);
            }
        }

        return result;
    }

    @SuppressWarnings("unchecked")
    private static <A> A newInstance(Class<A> type, Object... params) {
        try {
            if (type.getConstructors().length == 0) {
                throw new IllegalArgumentException("No constructor found in class " + type);
            }

            return (A) type.getConstructors()[0].newInstance(params);
        } catch (Exception e) {
            return sneakyThrow(e);
        }
    }

    @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"})
    private static <T, E extends Throwable> T sneakyThrow(Throwable e) throws E {
        throw (E) e;
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants