001/* 002 * Copyright (C) 2026 The Guava Authors 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package com.google.common.testing; 018 019import static com.google.common.base.StandardSystemProperty.JAVA_SPECIFICATION_VERSION; 020import static com.google.common.collect.ImmutableList.toImmutableList; 021import static com.google.common.reflect.Reflection.initialize; 022import static com.google.common.testing.ClassPathUtil.getClassPathUrls; 023 024import com.google.common.annotations.GwtIncompatible; 025import com.google.common.annotations.J2ktIncompatible; 026import com.google.common.collect.ImmutableList; 027import com.google.common.reflect.ClassPath; 028import com.google.common.reflect.ClassPath.ClassInfo; 029import com.google.errorprone.annotations.MustBeClosed; 030import com.google.j2objc.annotations.J2ObjCIncompatible; 031import java.io.Closeable; 032import java.io.IOException; 033import java.net.URLClassLoader; 034import junit.framework.TestCase; 035import org.jspecify.annotations.NullMarked; 036 037/** 038 * Abstract test class to be extended in any package in order to test that every class in that 039 * package is safe to be the first class to be initialized. 040 * 041 * <p>While Guava has <a href="https://github.com/google/guava/issues/1977">resolved</a> the known 042 * issues of this kind that were possible to trigger through publicly accessible APIs, it is 043 * possible for <a href="https://github.com/google/guava/issues/8617">those issues</a> to arise in 044 * other ways: 045 * 046 * <ul> 047 * <li>unexplained issues seen on Android: <a 048 * href="http://issuetracker.google.com/issues/40051933#comment6">Chromium issue 40051933's 049 * comment #6</a>, 050 * <li><a 051 * href="https://github.com/GoogleCloudPlatform/cloud-sql-jdbc-socket-factory/issues/2178#issuecomment-5346970152">GraalVM 052 * build-time initialization</a> 053 * <li>presumably other usages of reflection, such as a custom serialization library 054 * </ul> 055 * 056 * <p>This test does <i>not</i> attempt to detect deadlocks that could arise with multithreading, 057 * but it might manage to do so in some cases anyway. 058 * 059 * @since 33.8.0 060 */ 061@GwtIncompatible 062@J2ktIncompatible 063@J2ObjCIncompatible 064@NullMarked 065public abstract class AbstractClassInitializationTest extends TestCase { 066 public final void testClassesInitialize() throws Exception { 067 String packageName = getPackageName(); 068 ImmutableList<ClassInfo> classes = findClassesInPackage(packageName); 069 assertFalse("No classes found in package " + packageName, classes.isEmpty()); 070 for (ClassInfo classInfo : classes) { 071 if (isIgnored(classInfo)) { 072 continue; 073 } 074 try (IsolatedClassLoader loader = createIsolatedClassLoader()) { 075 Class<?> clazz = loader.loadClass(classInfo.getName()); 076 initialize(clazz); 077 } catch (Throwable t) { 078 throw new AssertionError("Failed to initialize " + classInfo, t); 079 } 080 } 081 } 082 083 private String getPackageName() { 084 return getClass().getPackage().getName(); 085 } 086 087 private static boolean isIgnored(ClassInfo classInfo) { 088 String name = classInfo.getName(); 089 return name.endsWith("Test") 090 || name.endsWith("Tests") 091 || name.endsWith("TestCase") 092 || name.endsWith("TestSuite") 093 || name.contains("Test$") 094 || name.contains("Tests$") 095 || name.contains("TestCase$") 096 || name.contains("TestSuite$") 097 || name.endsWith(".package-info") 098 || name.equals("package-info") 099 || name.endsWith(".module-info") 100 || name.equals("module-info") 101 // JDK8 has no VarHandle, so skip VarHandleAtomicHelper and VarHandleLittleEndianBytes there 102 || (isJava8() && name.contains("VarHandle")); 103 } 104 105 @MustBeClosed 106 private static IsolatedClassLoader createIsolatedClassLoader() { 107 ClassLoader parent = AbstractClassInitializationTest.class.getClassLoader(); 108 URLClassLoader urlClassLoader = 109 URLClassLoader.newInstance(getClassPathUrls(), /* parent= */ null); 110 return new IsolatedClassLoader(parent, urlClassLoader); 111 } 112 113 private static final class IsolatedClassLoader extends ClassLoader implements Closeable { 114 final URLClassLoader delegate; 115 116 IsolatedClassLoader(ClassLoader parent, URLClassLoader delegate) { 117 super(parent); 118 this.delegate = delegate; 119 } 120 121 @Override 122 protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { 123 if (name.startsWith("com.google.common.")) { 124 Class<?> c = findLoadedClass(name); 125 if (c == null) { 126 c = delegate.loadClass(name); 127 } 128 if (resolve) { 129 resolveClass(c); 130 } 131 return c; 132 } 133 return super.loadClass(name, resolve); 134 } 135 136 @Override 137 public void close() throws IOException { 138 delegate.close(); 139 } 140 } 141 142 private ImmutableList<ClassInfo> findClassesInPackage(String packageName) throws IOException { 143 return ClassPath.from(getClass().getClassLoader()).getAllClasses().stream() 144 .filter(classInfo -> classInfo.getPackageName().equals(packageName)) 145 .collect(toImmutableList()); 146 } 147 148 private static boolean isJava8() { 149 return JAVA_SPECIFICATION_VERSION.value().equals("1.8"); 150 } 151}