< prev index next > test/jdk/tools/jimage/VerifyJimage.java
Print this page
/*
! * Copyright (c) 2014, 2023, 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.
/*
! * 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 java.io.IOException;
import java.io.UncheckedIOException;
! import java.nio.file.DirectoryStream;
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.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 jdk.internal.jimage.BasicImageReader;
- import jdk.internal.jimage.ImageLocation;
/*
! * @test
! * @summary Verify jimage
* @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.
*/
! public class VerifyJimage {
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");
if (Files.notExists(bootimagePath)) {
! System.out.println("Test skipped, not an images build");
- return;
}
! long start = System.nanoTime();
! int numThreads = Integer.getInteger("jdk.test.threads", 1);
! JImageReader reader = newJImageReader();
! VerifyJimage verify = new VerifyJimage(reader, numThreads);
if (args.length == 0) {
! // load classes from jimage
- verify.loadClasses();
} else {
! Path dir = Paths.get(args[0]);
! if (Files.notExists(dir) || !Files.isDirectory(dir)) {
! throw new RuntimeException("Invalid argument: " + dir);
}
! verify.compareExplodedModules(dir);
}
! verify.waitForCompletion();
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);
- }
if (!failed.isEmpty()) {
throw new AssertionError("Test failed");
}
}
! private final AtomicInteger count = new AtomicInteger(0);
! private final JImageReader reader;
! private final ExecutorService pool;
! 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);
- }
! 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);
- }
- }
- });
}
}
}
- }
! 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;
- }
! if (reader.findLocation(entry) != null) {
! reader.compare(entry, p);
! }
! }
! 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");
- }
- });
- }
! private String toClassName(String entry) {
! int index = entry.indexOf('/', 1);
! return entry.substring(index + 1, entry.length())
! .replaceAll("\\.class$", "").replace('/', '.');
! }
! // 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");
! 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;
}
- 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);
! }
! static class JImageReader extends BasicImageReader {
- final Path jimage;
- JImageReader(Path p) throws IOException {
- super(p);
- this.jimage = p;
- }
! String imageName() {
! return jimage.getFileName().toString();
}
! int entries() {
! return getHeader().getTableLength();
}
! void compare(String entry, Path p) {
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);
}
}
}
}
* 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 jdk.internal.jimage.BasicImageReader;
+ import jtreg.SkippedException;
+
import java.io.IOException;
import java.io.UncheckedIOException;
! 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.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 static java.util.stream.Collectors.joining;
/*
! * @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
*/
! /*
! * @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 abstract class VerifyJimage implements Runnable {
private static final String MODULE_INFO = "module-info.class";
public static void main(String... args) throws Exception {
! // 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)) {
! throw new SkippedException("No boot image: " + bootimagePath);
}
! 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) {
! verifier = new ClassLoadingVerifier(modules, modulesRoot);
} else {
! 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);
}
! int maxThreads = Integer.getInteger("jdk.test.threads", 1);
+ verifier = new DirectoryContentVerifier(modules, rootDir, maxThreads, bootimagePath);
}
! 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();
!
! 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 static final class DirectoryContentVerifier extends VerifyJimage {
! private final Path rootDir;
! private final ExecutorService pool;
+ private final Path jimagePath;
! DirectoryContentVerifier(List<String> modules, Path rootDir, int maxThreads, Path jimagePath) {
! super(modules);
! this.rootDir = rootDir;
! this.pool = Executors.newFixedThreadPool(maxThreads);
! this.jimagePath = jimagePath;
! }
! @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();
}
}
! /**
! * 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<>();
! 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());
+ }
! @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));
! }
! 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);
+ }
+ }
! /**
! * 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");
+ }
! /**
! * 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("/", "/", ""));
+ }
}
}
! /**
! * 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";
! private final Path modulesRoot;
! ClassLoadingVerifier(List<String> modules, Path modulesRoot) {
! super(modules);
+ this.modulesRoot = modulesRoot;
}
! @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);
+ }
+ }
}
! private void loadClass(String cn, ClassLoader loader) {
try {
! 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 >