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}