Skip to content

Introduce Converter in junit-platform-commons #4219

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

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

scordio
Copy link
Contributor

@scordio scordio commented Dec 23, 2024

Overview


I hereby agree to the terms of the JUnit Contributor License Agreement.


Definition of Done

@scordio
Copy link
Contributor Author

scordio commented Dec 23, 2024

There is plenty of work to do 🙃

The current highlights:

Any feedback would be highly appreciated!

@scordio scordio force-pushed the conversion-service branch 3 times, most recently from 3304ad5 to c886b7a Compare December 31, 2024 11:26
@marcphilipp
Copy link
Member

Thanks for the draft! 👍

The tests are failing due to:

org.junit.platform.commons.support.conversion.ConversionService: module org.junit.platform.commons does not declare uses

That's because junit-platform-commons/src/module/org.junit.platform.commons/module-info.java is missing

uses org.junit.platform.commons.support.conversion.ConversionService;

Copy link
Member

@marcphilipp marcphilipp left a comment

Choose a reason for hiding this comment

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

Looks very promising! 👍


import org.junit.platform.commons.support.conversion.TypedConversionService;

// FIXME delete
Copy link
Member

Choose a reason for hiding this comment

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

This would make a good test case, though. We have existing tests that register services for tests using an extra class loader:

private static void withTestServices(Runnable runnable) {
var current = Thread.currentThread().getContextClassLoader();
var url = LauncherFactoryTests.class.getClassLoader().getResource("testservices/");
try (var classLoader = new URLClassLoader(new URL[] { url }, current)) {
Thread.currentThread().setContextClassLoader(classLoader);
runnable.run();
}
catch (IOException e) {
throw new UncheckedIOException(e);
}
finally {
Thread.currentThread().setContextClassLoader(current);
}
}

We could generalize and move that method to a test utility class (e.g. in junit-jupiter-api/src/testFixtures) so it can be reused here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Another possible integration test could be inspired by #3605.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We could generalize and move that method to a test utility class (e.g. in junit-jupiter-api/src/testFixtures) so it can be reused here.

@marcphilipp fine if I do it in a separate PR? Mostly to keep the size of this one under control 🙃

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Raised #4544.

@marcphilipp
Copy link
Member

That's because junit-platform-commons/src/module/org.junit.platform.commons/module-info.java is missing

uses org.junit.platform.commons.support.conversion.ConversionService;

When you add that, you'll also have to add it to platform-tooling-support-tests/projects/jar-describe-module/junit-platform-commons.expected.txt to adjust the integration test.

@scordio scordio force-pushed the conversion-service branch 2 times, most recently from 2e17c2b to 0c2faa7 Compare January 2, 2025 16:35
@scordio
Copy link
Contributor Author

scordio commented Jan 15, 2025

I've been lagging behind with this one but I should be able to spend time on it in the upcoming weekend.

@scordio
Copy link
Contributor Author

scordio commented Apr 21, 2025

Would you like to include this in 5.13 too? I should have enough time in the upcoming days to finalize it.

@scordio scordio force-pushed the conversion-service branch 3 times, most recently from 0a36152 to e380b8f Compare April 27, 2025 11:33
@scordio

This comment was marked as outdated.

@scordio
Copy link
Contributor Author

scordio commented May 5, 2025

I've hidden my last comment as it's mostly outdated after @sbrannen's one, and

supporting generics and something like a TypeDescriptor parameter object would be a good step in that direction.

If you'd like to go this way, I'm happy to adjust my work in this direction!

@marcphilipp
Copy link
Member

I agree with @sbrannen. Let's go with the generic version and the parameter object.

@scordio
Copy link
Contributor Author

scordio commented May 10, 2025

I think TypeDescriptor shouldn't be specific to the conversion feature, so I plan to define it in org.junit.platform.commons.

Any better ideas?

EDIT: or maybe it is? 🤔 looking at how the Spring Framework does it.

@sbrannen
Copy link
Member

I think TypeDescriptor shouldn't be specific to the conversion feature, so I plan to define it in org.junit.platform.commons.

I think it's OK for TypeDescriptor to reside in org.junit.platform.commons.support.conversion.

@scordio
Copy link
Contributor Author

scordio commented May 10, 2025

@sbrannen I'm still trying to get a proper background before jumping on the actual changes, so here's a dumb question 🙂

Specifically about parameterized types, is the ResolvableType abstraction closer to what we would need, from your perspective? Or maybe you imagine that together with TypeDescriptor?

@marcphilipp
Copy link
Member

To start with, I had thought a relatively simple interface should suffice for TypeDescriptor.

interface TypeDescriptor {
	Class<?> getType();
}

@sbrannen WDYT?

@scordio
Copy link
Contributor Author

scordio commented May 10, 2025

My suggestion is not to close the door for Type getType() so maybe Class<?> getRawClass() or similar?

Mostly to be ready for cases where TypeDescriptor might wrap a ParameterizedType.

@marcphilipp
Copy link
Member

We could do what Java did (e.g. for Method.getReturnType()) and have Class<?> getType() now and potentially Type getGenericType() later.

@scordio
Copy link
Contributor Author

scordio commented May 11, 2025

To start with, I had thought a relatively simple interface should suffice for TypeDescriptor.

interface TypeDescriptor {
	Class<?> getType();
}

So far, I understood TypeDescriptor should replace the Class parameter in ConversionSupport.convert (and, consequently, in the Converter methods), so:

public static <T> T convert(Object source, Class<T> targetType, ClassLoader classLoader)

should become:

public static <T> T convert(Object source, TypeDescriptor targetType, ClassLoader classLoader)

Therefore, I planned to define TypeDescriptor as a public class, with a private constructor and factory methods like forField(Field) and forParameter(Parameter) that ConversionSupport users could rely on (including DefaultArgumentConverter).

@marcphilipp did you have something else in mind?

@scordio scordio force-pushed the conversion-service branch from 45effd9 to a8d8356 Compare May 11, 2025 13:00
@scordio scordio changed the title Introduce ConversionService in junit-platform-commons Introduce Converter in junit-platform-commons May 11, 2025
@scordio scordio force-pushed the conversion-service branch from a8d8356 to d3675e2 Compare May 11, 2025 13:03
@scordio
Copy link
Contributor Author

scordio commented May 11, 2025

After renaming to Converter, I realized StringToObjectConverter is pretty much a similar abstraction.

I propose to refactor the StringToObjectConverter hierarchy and rewrite all classes as Converter implementations (still with package-private visibility and used by DefaultConverter only).

WDYT?

@sbrannen
Copy link
Member

I propose to refactor the StringToObjectConverter hierarchy and rewrite all classes as Converter implementations (still with package-private visibility and used by DefaultConverter only).

WDYT?

That's pretty much the vision I had all along: to have single abstraction/API.

So, yes, that sounds good to me. 👍

Though... the devil is in details. 😉

Comment on lines +47 to +56
public Class<?> getType() {
return type;
}
Copy link
Contributor Author

@scordio scordio May 11, 2025

Choose a reason for hiding this comment

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

For now, most of the code relies on getType(), but I plan to check if dedicated APIs could better encapsulate some use cases (similar to getWrapperType or isPrimitive).

}
return null;
}
return convert(source, TypeDescriptor.forType(targetType), getClassLoader(classLoader));
Copy link
Contributor Author

@scordio scordio May 11, 2025

Choose a reason for hiding this comment

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

I wonder if the deprecated convert should delegate to the other convert method or should invoke DefaultConverter directly, skipping the new service loader logic. For now, I went with the former.

* type is a reference type
* @throws ConversionException if an error occurs during the conversion
*/
protected abstract T convert(S source) throws ConversionException;
Copy link
Contributor Author

@scordio scordio May 11, 2025

Choose a reason for hiding this comment

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

Should this carry the target TypeDescriptor too?

Suggested change
protected abstract T convert(S source) throws ConversionException;
protected abstract T convert(S source, TypeDescriptor targetType) throws ConversionException;

Copy link
Member

Choose a reason for hiding this comment

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

Yes, I think so as well as sourceType and classLoader.

* <p>This method will only be invoked in {@link #canConvertTo(Class)}
* returned {@code true} for the same target type.
*/
Object convert(String source, Class<?> targetType) throws Exception;
Copy link
Contributor Author

@scordio scordio May 11, 2025

Choose a reason for hiding this comment

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

Being an internal API, I deleted this convert(String, Class<?>) variant to reduce complexity. The downside is that the ClassLoader parameter is mostly unused in the subclasses.

* @see TypedConverter
*/
@API(status = EXPERIMENTAL, since = "1.13")
public interface Converter {
Copy link
Member

Choose a reason for hiding this comment

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

Didn't we decide to make this generic as in Converter<S, T>?

Copy link
Member

Choose a reason for hiding this comment

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

I guess for that to make sense, we'd have to parameterize TypeDescriptor as well.

@scordio @sbrannen WDYT?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I had the same in mind but wasn't entirely confident about this direction yesterday, especially before refactoring StringToObjectConversion.

But now the refactoring is done so I can sketch it and see what happens 🙂

@scordio scordio force-pushed the conversion-service branch from 6001a2c to 5190e37 Compare May 16, 2025 06:19
Comment on lines 31 to 48
public static TypeDescriptor forClass(Class<?> clazz) {
return new TypeDescriptor(clazz);
}

public static TypeDescriptor forInstance(Object instance) {
return new TypeDescriptor(instance.getClass());
}

public static TypeDescriptor forField(Field field) {
return new TypeDescriptor(field.getType());
}

public static TypeDescriptor forParameter(Parameter parameter) {
return new TypeDescriptor(parameter.getType());
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

General null handling is currently missing

Comment on lines +64 to +82
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
TypeDescriptor that = (TypeDescriptor) o;
return this.type.equals(that.type);
}

@Override
public int hashCode() {
return this.type.hashCode();
}
Copy link
Contributor Author

@scordio scordio May 16, 2025

Choose a reason for hiding this comment

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

I noticed EqualsAndHashCodeAssertions under junit-jupiter-api to test equals/hashCode. It seems I can use the same in junit-platform-commons.

Out of curiosity, was EqualsVerifier ever considered for such tests?

Copy link
Member

Choose a reason for hiding this comment

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

I noticed EqualsAndHashCodeAssertions under junit-jupiter-api to test equals/hashCode. It seems I can use the same in junit-platform-commons.

👍

Out of curiosity, was EqualsVerifier ever considered for such tests?

I don't think it was discussed but it might be a good addition or even replacement but probably in a separate PR.

@scordio scordio force-pushed the conversion-service branch from 5190e37 to af01791 Compare May 16, 2025 17:14
@scordio scordio force-pushed the conversion-service branch from af01791 to 0034d7d Compare May 16, 2025 17:40
@scordio scordio mentioned this pull request May 17, 2025
6 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Introduce generic ConversionService SPI
3 participants