HHH-18284 Test StaticClassLists more extensively

This commit is contained in:
Yoann Rodière 2024-06-19 18:48:27 +02:00 committed by Sanne Grinovero
parent e9772c5c02
commit a1962fd629
4 changed files with 310 additions and 6 deletions

View File

@ -15,4 +15,5 @@ dependencies {
compileOnly "org.graalvm.sdk:graal-sdk:22.2.0"
testImplementation project( ':hibernate-core' )
testImplementation libs.jandex
}

View File

@ -0,0 +1,56 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later
* See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html
*/
package org.hibernate.graalvm.internal;
import java.io.Closeable;
import java.io.IOException;
import java.net.URI;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Map;
class CodeSource implements Closeable {
public static CodeSource open(URI location) throws IOException {
if ( "jar".equals( location.getScheme() ) ) {
var fs = FileSystems.newFileSystem( location, Map.of() );
return new CodeSource( fs, fs.getRootDirectories().iterator().next() );
}
else if ( "file".equals( location.getScheme() ) && location.getPath().endsWith( ".jar" ) ) {
location = URI.create( "jar:" + location );
var fs = FileSystems.newFileSystem( location, Map.of() );
return new CodeSource( fs, fs.getRootDirectories().iterator().next() );
}
else if ( "file".equals( location.getScheme() ) ) {
return new CodeSource( null, Paths.get( location ) );
}
else {
throw new IllegalArgumentException( "Unsupported URI: " + location );
}
}
private final FileSystem toClose;
private final Path root;
private CodeSource(FileSystem toClose, Path root) {
this.toClose = toClose;
this.root = root;
}
@Override
public void close() throws IOException {
if ( toClose != null ) {
toClose.close();
}
}
public Path getRoot() {
return root;
}
}

View File

@ -0,0 +1,95 @@
/*
* SPDX-License-Identifier: Apache-2.0
* Copyright Red Hat Inc. and Hibernate Authors
*/
package org.hibernate.graalvm.internal;
import static org.assertj.core.api.Assertions.assertThat;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Modifier;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.DotName;
import org.jboss.jandex.Index;
import org.jboss.jandex.Indexer;
public final class JandexTestUtils {
private JandexTestUtils() {
}
public static Index indexJar(Class<?> clazz) {
return indexClasses( determineJarLocation( clazz ) );
}
private static URI determineJarLocation(Class<?> clazz) {
URL url = clazz.getProtectionDomain().getCodeSource().getLocation();
try {
return url.toURI();
}
catch (URISyntaxException e) {
throw new IllegalStateException( "Cannot retrieve URI for JAR of " + clazz + "?", e );
}
}
private static Index indexClasses(URI classesUri) {
try ( CodeSource cs = CodeSource.open( classesUri ) ) {
Indexer indexer = new Indexer();
try ( Stream<Path> stream = Files.walk( cs.getRoot() ) ) {
for ( Iterator<Path> it = stream.iterator(); it.hasNext(); ) {
Path path = it.next();
if ( path.getFileName() == null || !path.getFileName().toString().endsWith( ".class" ) ) {
continue;
}
try ( InputStream inputStream = Files.newInputStream( path ) ) {
indexer.index( inputStream );
}
}
}
return indexer.complete();
}
catch (RuntimeException | IOException e) {
throw new IllegalStateException( "Cannot index classes at " + classesUri, e );
}
}
public static Class<?> load(DotName className) {
try {
return Class.forName( className.toString() );
}
catch (ClassNotFoundException e) {
throw new RuntimeException( "Could not load class " + className, e );
}
}
public static Set<Class<?>> findConcreteNamedImplementors(Index index, Class<?>... interfaces) {
return Arrays.stream( interfaces ).map( DotName::createSimple )
.flatMap( n -> findConcreteNamedImplementors( index, n ).stream() )
.collect( Collectors.toSet() );
}
private static Set<Class<?>> findConcreteNamedImplementors(Index index, DotName interfaceDotName) {
assertThat( index.getClassByName( interfaceDotName ) ).isNotNull();
return index.getAllKnownImplementors( interfaceDotName ).stream()
.filter( c -> !c.isInterface()
// Ignore anonymous classes
&& c.simpleName() != null )
.map( ClassInfo::name )
.map( JandexTestUtils::load )
.filter( c -> ( c.getModifiers() & Modifier.ABSTRACT ) == 0 )
.collect( Collectors.toSet() );
}
}

View File

@ -7,35 +7,187 @@
package org.hibernate.graalvm.internal;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hibernate.graalvm.internal.JandexTestUtils.findConcreteNamedImplementors;
import java.io.IOException;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.util.Arrays;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.hibernate.Session;
import org.hibernate.event.spi.EventType;
import org.hibernate.internal.util.ReflectHelper;
import org.hibernate.persister.collection.CollectionPersister;
import org.hibernate.persister.entity.EntityPersister;
import org.junit.Assert;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import org.jboss.jandex.Index;
public class StaticClassListsTest {
private static Index hibernateIndex;
@BeforeAll
public static void index() throws IOException {
hibernateIndex = JandexTestUtils.indexJar( Session.class );
}
@Nested
class TypesNeedingAllConstructorsAccessible {
@ParameterizedTest
@EnumSource(TypesNeedingAllConstructorsAccessible_Category.class)
void containsAllExpectedClasses(TypesNeedingAllConstructorsAccessible_Category category) {
assertThat( StaticClassLists.typesNeedingAllConstructorsAccessible() )
.containsAll( category.classes().collect( Collectors.toSet() ) );
}
@Test
void meta_noMissingTestCategory() {
assertThat( Arrays.stream( TypesNeedingAllConstructorsAccessible_Category.values() ).flatMap( TypesNeedingAllConstructorsAccessible_Category::classes ) )
.as( "If this fails, a category is missing in " + TypesNeedingAllConstructorsAccessible_Category.class )
.contains( StaticClassLists.typesNeedingAllConstructorsAccessible() );
}
}
// TODO ORM 7: Move this inside TypesNeedingAllConstructorsAccessible (requires JDK 17) and rename to simple Category
enum TypesNeedingAllConstructorsAccessible_Category {
PERSISTERS {
@Override
Stream<Class<?>> classes() {
return findConcreteNamedImplementors(
hibernateIndex, EntityPersister.class, CollectionPersister.class )
.stream();
}
},
MISC {
@Override
Stream<Class<?>> classes() {
// NOTE: Please avoid putting anything here, it's really a last resort.
// Ideally you'd rather add new categories with their own way of listing classes,
// like in PERSISTERS.
// Putting anything here is running the risk of forgetting
// why it was necessary in the first place...
return Stream.of(
// Logging - sometimes looked up without a static field
org.hibernate.internal.CoreMessageLogger_$logger.class
);
}
};
abstract Stream<Class<?>> classes();
}
@Nested
class TypesNeedingDefaultConstructorAccessible {
@ParameterizedTest
@EnumSource(TypesNeedingDefaultConstructorAccessible_Category.class)
void containsAllExpectedClasses(TypesNeedingDefaultConstructorAccessible_Category category) {
assertThat( StaticClassLists.typesNeedingDefaultConstructorAccessible() )
.containsAll( category.classes().collect( Collectors.toSet() ) );
}
@Test
void meta_noMissingTestCategory() {
assertThat( Arrays.stream( TypesNeedingDefaultConstructorAccessible_Category.values() ).flatMap( TypesNeedingDefaultConstructorAccessible_Category::classes ) )
.as( "If this fails, a category is missing in " + TypesNeedingDefaultConstructorAccessible_Category.class )
.contains( StaticClassLists.typesNeedingDefaultConstructorAccessible() );
}
}
// TODO ORM 7: Move this inside TypesNeedingDefaultConstructorAccessible (requires JDK 17) and rename to simple Category
enum TypesNeedingDefaultConstructorAccessible_Category {
MISC {
@Override
Stream<Class<?>> classes() {
// NOTE: Please avoid putting anything here, it's really a last resort.
// Ideally you'd rather add new categories with their own way of listing classes,
// like in PERSISTERS.
// Putting anything here is running the risk of forgetting
// why it was necessary in the first place...
return Stream.of(
org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorBuilderImpl.class,
org.hibernate.id.enhanced.SequenceStyleGenerator.class,
org.hibernate.boot.model.naming.ImplicitNamingStrategyJpaCompliantImpl.class,
org.hibernate.resource.transaction.backend.jta.internal.JtaTransactionCoordinatorBuilderImpl.class,
org.hibernate.type.EnumType.class,
org.hibernate.tool.schema.internal.script.MultiLineSqlScriptExtractor.class
);
}
};
abstract Stream<Class<?>> classes();
}
@Nested
class TypesNeedingArrayCopy {
@Test
void containsEventListenerInterfaces() {
@ParameterizedTest
@EnumSource(TypesNeedingArrayCopy_Category.class)
void containsAllExpectedClasses(TypesNeedingArrayCopy_Category category) {
assertThat( StaticClassLists.typesNeedingArrayCopy() )
.containsAll( eventListenerInterfaces().collect( Collectors.toSet() ) );
.containsAll( category.classes().collect( Collectors.toSet() ) );
}
static Stream<Class<?>> eventListenerInterfaces() {
return EventType.values().stream().map( EventType::baseListenerInterface )
.map( c -> Array.newInstance( c, 0 ).getClass() );
@Test
void meta_noMissingTestCategory() {
assertThat( Arrays.stream( TypesNeedingArrayCopy_Category.values() ).flatMap( TypesNeedingArrayCopy_Category::classes ) )
.as( "If this fails, a category is missing in " + TypesNeedingArrayCopy_Category.class )
.contains( StaticClassLists.typesNeedingArrayCopy() );
}
}
// TODO ORM 7: Move this inside TypesNeedingArrayCopy (requires JDK 17) and rename to simple Category
enum TypesNeedingArrayCopy_Category {
EVENT_LISTENER_INTERFACES {
@Override
Stream<Class<?>> classes() {
return EventType.values().stream().map( EventType::baseListenerInterface )
.map( c -> Array.newInstance( c, 0 ).getClass() );
}
},
MISC {
@Override
Stream<Class<?>> classes() {
// NOTE: Please avoid putting anything here, it's really a last resort.
// Ideally you'd rather add new categories with their own way of listing classes,
// like in EVENT_LISTENER_INTERFACES.
// Putting anything here is running the risk of forgetting
// why it was necessary in the first place...
return Stream.of(
// Java classes -- the why is lost to history
java.util.function.Function[].class,
java.util.List[].class,
java.util.Map.Entry[].class,
java.util.function.Supplier[].class,
// Graphs -- the why is lost to history
org.hibernate.graph.spi.AttributeNodeImplementor[].class,
org.hibernate.sql.results.graph.FetchParent[].class,
org.hibernate.graph.spi.GraphImplementor[].class,
org.hibernate.graph.internal.parse.SubGraphGenerator[].class,
// AST/parsing -- no way to detect this automatically, you just have to know.
org.hibernate.sql.ast.Clause[].class,
org.hibernate.query.hql.spi.DotIdentifierConsumer[].class,
org.hibernate.query.sqm.sql.FromClauseIndex[].class,
org.hibernate.query.sqm.spi.ParameterDeclarationContext[].class,
org.hibernate.sql.ast.tree.select.QueryPart[].class,
org.hibernate.sql.ast.spi.SqlAstProcessingState[].class,
org.hibernate.query.hql.spi.SqmCreationProcessingState[].class,
org.hibernate.sql.ast.tree.Statement[].class,
// Various internals -- the why is lost to history
org.hibernate.sql.results.jdbc.spi.JdbcValuesSourceProcessingState[].class
);
}
};
abstract Stream<Class<?>> classes();
}
@Nested
class BasicConstructorsAvailable {