< prev index next >

test/jdk/tools/jimage/VerifyJimage.java

Print this page
@@ -1,7 +1,7 @@
  /*
-  * 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.

@@ -19,227 +19,320 @@
   * 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 >