< prev index next > test/jdk/tools/jimage/VerifyJimage.java
Print this page
/*
- * Copyright (c) 2014, 2023, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2014, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
- import java.io.File;
+ import jdk.internal.jimage.BasicImageReader;
+ import jtreg.SkippedException;
+
import java.io.IOException;
import java.io.UncheckedIOException;
- import java.nio.file.DirectoryStream;
+ import java.net.URI;
+ import java.nio.file.FileSystem;
+ import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
- import java.nio.file.Paths;
- import java.nio.file.attribute.BasicFileAttributes;
- import java.util.ArrayList;
import java.util.Arrays;
import java.util.Deque;
+ import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import java.util.stream.Stream;
+ import java.util.stream.StreamSupport;
- import jdk.internal.jimage.BasicImageReader;
- import jdk.internal.jimage.ImageLocation;
+ import static java.util.stream.Collectors.joining;
/*
- * @test
- * @summary Verify jimage
+ * @test id=load
+ * @summary Load all classes defined in JRT file system.
+ * @library /test/lib
* @modules java.base/jdk.internal.jimage
* @run main/othervm --add-modules ALL-SYSTEM VerifyJimage
*/
- /**
- * This test runs in two modes:
- * (1) No argument: it verifies the jimage by loading all classes in the runtime
- * (2) path of exploded modules: it compares bytes of each file in the exploded
- * module with the entry in jimage
- *
- * FIXME: exception thrown when findLocation from jimage by multiple threads
- * -Djdk.test.threads=<n> to specify the number of threads.
+ /*
+ * @test id=compare
+ * @summary Compare an exploded directory of module classes with the system jimage.
+ * @library /test/lib
+ * @modules java.base/jdk.internal.jimage
+ * @run main/othervm --add-modules ALL-SYSTEM -Djdk.test.threads=10 VerifyJimage ../../jdk/modules
*/
- public class VerifyJimage {
+ public abstract class VerifyJimage implements Runnable {
private static final String MODULE_INFO = "module-info.class";
- private static final Deque<String> failed = new ConcurrentLinkedDeque<>();
public static void main(String... args) throws Exception {
-
- String home = System.getProperty("java.home");
- Path bootimagePath = Paths.get(home, "lib", "modules");
+ // Best practice is to read "test.jdk" in preference to "java.home".
+ String testJdk = System.getProperty("test.jdk", System.getProperty("java.home"));
+ Path jdkRoot = Path.of(testJdk);
+ Path bootimagePath = jdkRoot.resolve("lib", "modules");
if (Files.notExists(bootimagePath)) {
- System.out.println("Test skipped, not an images build");
- return;
+ throw new SkippedException("No boot image: " + bootimagePath);
}
- long start = System.nanoTime();
- int numThreads = Integer.getInteger("jdk.test.threads", 1);
- JImageReader reader = newJImageReader();
- VerifyJimage verify = new VerifyJimage(reader, numThreads);
+ FileSystem jrtFs = FileSystems.getFileSystem(URI.create("jrt:/"));
+ Path modulesRoot = jrtFs.getPath("/").resolve("modules");
+ List<String> modules;
+ try (Stream<Path> moduleDirs = Files.list(modulesRoot)) {
+ modules = moduleDirs.map(Path::getFileName).map(Object::toString).toList();
+ }
+ VerifyJimage verifier;
if (args.length == 0) {
- // load classes from jimage
- verify.loadClasses();
+ verifier = new ClassLoadingVerifier(modules, modulesRoot);
} else {
- Path dir = Paths.get(args[0]);
- if (Files.notExists(dir) || !Files.isDirectory(dir)) {
- throw new RuntimeException("Invalid argument: " + dir);
+ Path pathArg = Path.of(args[0].replace("/", FileSystems.getDefault().getSeparator()));
+ // The path argument may be relative.
+ Path rootDir = jdkRoot.resolve(pathArg);
+ if (!Files.isDirectory(rootDir)) {
+ throw new SkippedException("No modules directory found: " + rootDir);
}
- verify.compareExplodedModules(dir);
+ int maxThreads = Integer.getInteger("jdk.test.threads", 1);
+ verifier = new DirectoryContentVerifier(modules, rootDir, maxThreads, bootimagePath);
}
- verify.waitForCompletion();
+ verifier.verify();
+ }
+
+ final List<String> modules;
+ // Count of items which have passed verification.
+ final AtomicInteger verifiedCount = new AtomicInteger(0);
+ // Error messages for verification failures.
+ final Deque<String> failed = new ConcurrentLinkedDeque<>();
+
+ private VerifyJimage(List<String> modules) {
+ this.modules = modules;
+ }
+
+ void verify() {
+ long start = System.nanoTime();
+ run();
long end = System.nanoTime();
- int entries = reader.entries();
- System.out.format("%d entries %d files verified: %d ms %d errors%n",
- entries, verify.count.get(),
- TimeUnit.NANOSECONDS.toMillis(end - start), failed.size());
- for (String f : failed) {
- System.err.println(f);
- }
+
+ System.out.format("Verified %d entries: %d ms, %d errors%n",
+ verifiedCount.get(),
+ TimeUnit.NANOSECONDS.toMillis(end - start),
+ failed.size());
if (!failed.isEmpty()) {
+ failed.forEach(System.err::println);
throw new AssertionError("Test failed");
}
}
- private final AtomicInteger count = new AtomicInteger(0);
- private final JImageReader reader;
- private final ExecutorService pool;
+ private static final class DirectoryContentVerifier extends VerifyJimage {
+ private final Path rootDir;
+ private final ExecutorService pool;
+ private final Path jimagePath;
- VerifyJimage(JImageReader reader, int numThreads) {
- this.reader = reader;
- this.pool = Executors.newFixedThreadPool(numThreads);
- }
-
- private void waitForCompletion() throws InterruptedException {
- pool.shutdown();
- pool.awaitTermination(20, TimeUnit.SECONDS);
- }
+ DirectoryContentVerifier(List<String> modules, Path rootDir, int maxThreads, Path jimagePath) {
+ super(modules);
+ this.rootDir = rootDir;
+ this.pool = Executors.newFixedThreadPool(maxThreads);
+ this.jimagePath = jimagePath;
+ }
- private void compareExplodedModules(Path dir) throws IOException {
- System.out.println("comparing jimage with " + dir);
-
- try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) {
- for (Path mdir : stream) {
- if (Files.isDirectory(mdir)) {
- pool.execute(new Runnable() {
- @Override
- public void run() {
- try {
- Files.find(mdir, Integer.MAX_VALUE, (Path p, BasicFileAttributes attr)
- -> !Files.isDirectory(p) &&
- !mdir.relativize(p).toString().startsWith("_") &&
- !p.getFileName().toString().equals("MANIFEST.MF"))
- .forEach(p -> compare(mdir, p, reader));
- } catch (IOException e) {
- throw new UncheckedIOException(e);
- }
- }
- });
+ @Override
+ public void run() {
+ System.out.println("Comparing jimage with: " + rootDir);
+ try (BasicImageReader jimage = BasicImageReader.open(jimagePath)) {
+ for (String modName : modules) {
+ Path modDir = rootDir.resolve(modName);
+ if (!Files.isDirectory(modDir)) {
+ failed.add("Missing module directory: " + modDir);
+ } else {
+ pool.execute(new ModuleResourceComparator(rootDir, modName, jimage));
+ }
+ }
+ pool.shutdown();
+ if (!pool.awaitTermination(20, TimeUnit.SECONDS)) {
+ failed.add("Directory verification timed out");
}
+ } catch (IOException ex) {
+ throw new UncheckedIOException(ex);
+ } catch (InterruptedException e) {
+ failed.add("Directory verification was interrupted");
+ Thread.currentThread().interrupt();
}
}
- }
- private final List<String> BOOT_RESOURCES = Arrays.asList(
- "java.base/META-INF/services/java.nio.file.spi.FileSystemProvider"
- );
- private final List<String> EXT_RESOURCES = Arrays.asList(
- "jdk.zipfs/META-INF/services/java.nio.file.spi.FileSystemProvider"
- );
- private final List<String> APP_RESOURCES = Arrays.asList(
- "jdk.hotspot.agent/META-INF/services/com.sun.jdi.connect.Connector",
- "jdk.jdi/META-INF/services/com.sun.jdi.connect.Connector"
- );
-
- private void compare(Path mdir, Path p, JImageReader reader) {
- String entry = p.getFileName().toString().equals(MODULE_INFO)
- ? mdir.getFileName().toString() + "/" + MODULE_INFO
- : mdir.relativize(p).toString().replace(File.separatorChar, '/');
-
- count.incrementAndGet();
- String file = mdir.getFileName().toString() + "/" + entry;
- if (APP_RESOURCES.contains(file)) {
- // skip until the service config file is merged
- System.out.println("Skipped " + file);
- return;
- }
+ /**
+ * Verifies the contents of the current runtime jimage file by comparing
+ * entries with the on-disk resources in a given directory.
+ */
+ private class ModuleResourceComparator implements Runnable {
+ private final Path rootDir;
+ private final String moduleName;
+ private final BasicImageReader jimage;
+ private final String moduleInfoName;
+ // Entries we expect to find in the jimage module.
+ private final Set<String> moduleEntries;
+ private final Set<String> handledEntries = new HashSet<>();
- if (reader.findLocation(entry) != null) {
- reader.compare(entry, p);
- }
- }
+ public ModuleResourceComparator(Path rootDir, String moduleName, BasicImageReader jimage) {
+ this.rootDir = rootDir;
+ this.moduleName = moduleName;
+ this.jimage = jimage;
+ String moduleEntryPrefix = "/" + moduleName + "/";
+ this.moduleInfoName = moduleEntryPrefix + MODULE_INFO;
+ this.moduleEntries =
+ Arrays.stream(jimage.getEntryNames())
+ .filter(n -> n.startsWith(moduleEntryPrefix))
+ .filter(n -> !isJimageOnly(n))
+ .collect(Collectors.toSet());
+ }
- private void loadClasses() {
- ClassLoader loader = ClassLoader.getSystemClassLoader();
- Stream.of(reader.getEntryNames())
- .filter(this::accept)
- .map(this::toClassName)
- .forEach(cn -> {
- count.incrementAndGet();
- try {
- System.out.println("Loading " + cn);
- Class.forName(cn, false, loader);
- } catch (VerifyError ve) {
- System.err.println("VerifyError for " + cn);
- failed.add(reader.imageName() + ": " + cn + " not verified: " + ve.getMessage());
- } catch (ClassNotFoundException e) {
- failed.add(reader.imageName() + ": " + cn + " not found");
- }
- });
- }
+ @Override
+ public void run() {
+ try (Stream<Path> files = Files.walk(rootDir.resolve(moduleName))) {
+ files.filter(this::shouldVerify).forEach(this::compareEntry);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ moduleEntries.stream()
+ .filter(n -> !handledEntries.contains(n))
+ .sorted()
+ .forEach(n -> failed.add("Untested jimage entry: " + n));
+ }
- private String toClassName(String entry) {
- int index = entry.indexOf('/', 1);
- return entry.substring(index + 1, entry.length())
- .replaceAll("\\.class$", "").replace('/', '.');
- }
+ void compareEntry(Path path) {
+ String entryName = getEntryName(path);
+ if (!moduleEntries.contains(entryName)) {
+ // Corresponds to an on-disk file which is not expected to
+ // be present in the jimage. This is normal and is skipped.
+ return;
+ }
+ // Mark valid entries as "handled" to track if we've seen them
+ // (even if we don't test their content).
+ if (!handledEntries.add(entryName)) {
+ failed.add("Duplicate entry name: " + entryName);
+ return;
+ }
+ if (isExpectedToDiffer(entryName)) {
+ return;
+ }
+ try {
+ int mismatch = Arrays.mismatch(
+ Files.readAllBytes(path),
+ jimage.getResource(entryName));
+ if (mismatch == -1) {
+ verifiedCount.incrementAndGet();
+ } else {
+ failed.add("Content diff (byte offset " + mismatch + "): " + entryName);
+ }
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
- // All JVMCI packages other than jdk.vm.ci.services are dynamically
- // exported to jdk.graal.compiler
- private static Set<String> EXCLUDED_MODULES = Set.of("jdk.graal.compiler");
+ /**
+ * Predicate for files which correspond to entries in the jimage.
+ *
+ * <p>This should be a narrow test with minimal chance of
+ * false-negative matching, primarily focusing on excluding build
+ * artifacts.
+ */
+ boolean shouldVerify(Path path) {
+ // Use the entry name because we know it uses the '/' separator.
+ String entryName = getEntryName(path);
+ return Files.isRegularFile(path)
+ && !entryName.contains("/_the.")
+ && !entryName.contains("/_element_lists.");
+ }
+
+ /**
+ * Predicate for the limited subset of entries which are expected to
+ * exist in the file system, but are not expected to have the same
+ * content as the associated jimage entry. This is to handle files
+ * which are modified/patched by jlink plugins.
+ *
+ * <p>This should be a narrow test with minimal chance of
+ * false-positive matching.
+ */
+ private boolean isExpectedToDiffer(String entryName) {
+ return entryName.equals(moduleInfoName)
+ || (entryName.startsWith("/java.base/java/lang/invoke/") && entryName.endsWith("$Holder.class"))
+ || entryName.equals("/java.base/jdk/internal/module/SystemModulesMap.class");
+ }
- private boolean accept(String entry) {
- int index = entry.indexOf('/', 1);
- String mn = index > 1 ? entry.substring(1, index) : "";
- if (mn.isEmpty() || EXCLUDED_MODULES.contains(mn)) {
- return false;
+ /**
+ * Predicate for the limited subset of entries which are not expected
+ * to exist in the file system, such as those created synthetically
+ * by jlink plugins.
+ *
+ * <p>This should be a narrow test with minimal chance of
+ * false-positive matching.
+ */
+ private boolean isJimageOnly(String entryName) {
+ return entryName.startsWith("/java.base/jdk/internal/module/SystemModules$")
+ || entryName.startsWith("/java.base/java/lang/invoke/BoundMethodHandle$Species_");
+ }
+
+ private String getEntryName(Path path) {
+ return StreamSupport.stream(rootDir.relativize(path).spliterator(), false)
+ .map(Object::toString).collect(joining("/", "/", ""));
+ }
}
- return entry.endsWith(".class") && !entry.endsWith(MODULE_INFO);
}
- private static JImageReader newJImageReader() throws IOException {
- String home = System.getProperty("java.home");
- Path jimage = Paths.get(home, "lib", "modules");
- System.out.println("opened " + jimage);
- return new JImageReader(jimage);
- }
+ /**
+ * Verifies the contents of the current runtime jimage file by attempting to
+ * load every available class based on the content of the JRT file system.
+ */
+ static final class ClassLoadingVerifier extends VerifyJimage {
+ private static final String CLASS_SUFFIX = ".class";
- static class JImageReader extends BasicImageReader {
- final Path jimage;
- JImageReader(Path p) throws IOException {
- super(p);
- this.jimage = p;
- }
+ private final Path modulesRoot;
- String imageName() {
- return jimage.getFileName().toString();
+ ClassLoadingVerifier(List<String> modules, Path modulesRoot) {
+ super(modules);
+ this.modulesRoot = modulesRoot;
}
- int entries() {
- return getHeader().getTableLength();
+ @Override
+ public void run() {
+ ClassLoader loader = ClassLoader.getSystemClassLoader();
+ for (String modName : modules) {
+ Path modDir = modulesRoot.resolve(modName);
+ try (Stream<Path> files = Files.walk(modDir)) {
+ files.map(modDir::relativize)
+ .filter(ClassLoadingVerifier::isClassFile)
+ .map(ClassLoadingVerifier::toClassName)
+ .forEach(cn -> loadClass(cn, loader));
+ } catch (IOException ex) {
+ throw new UncheckedIOException(ex);
+ }
+ }
}
- void compare(String entry, Path p) {
+ private void loadClass(String cn, ClassLoader loader) {
try {
- byte[] bytes = Files.readAllBytes(p);
- byte[] imagebytes = getResource(entry);
- if (!Arrays.equals(bytes, imagebytes)) {
- failed.add(imageName() + ": bytes differs than " + p.toString());
- }
- } catch (IOException e) {
- throw new UncheckedIOException(e);
+ Class.forName(cn, false, loader);
+ verifiedCount.incrementAndGet();
+ } catch (VerifyError ve) {
+ System.err.println("VerifyError for " + cn);
+ failed.add("Class: " + cn + " not verified: " + ve.getMessage());
+ } catch (ClassNotFoundException e) {
+ failed.add("Class: " + cn + " not found");
}
}
+
+ /**
+ * Maps a module-relative JRT path of a class file to its corresponding
+ * fully-qualified class name.
+ */
+ private static String toClassName(Path path) {
+ // JRT uses '/' as the separator, and relative paths don't start with '/'.
+ String s = path.toString();
+ return s.substring(0, s.length() - CLASS_SUFFIX.length()).replace('/', '.');
+ }
+
+ /** Whether a module-relative JRT file system path is a class file. */
+ private static boolean isClassFile(Path path) {
+ String classFileName = path.getFileName().toString();
+ return classFileName.endsWith(CLASS_SUFFIX)
+ && !classFileName.equals(MODULE_INFO);
+ }
}
}
< prev index next >