< prev index next >

test/jdk/tools/jimage/VerifyJimage.java

Print this page
*** 1,7 ***
  /*
!  * 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.
--- 1,7 ---
  /*
!  * 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 ***
   * 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);
              }
          }
      }
  }
--- 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 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 >