diff --git a/test/jdk/tools/jimage/VerifyJimage.java b/test/jdk/tools/jimage/VerifyJimage.java index 6da0e719950db..08f567cbecb53 100644 --- a/test/jdk/tools/jimage/VerifyJimage.java +++ b/test/jdk/tools/jimage/VerifyJimage.java @@ -1,5 +1,5 @@ /* - * 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 @@ -21,17 +21,19 @@ * 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; @@ -41,205 +43,296 @@ 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= 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 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 modules; + try (Stream 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 modules; + // Count of items which have passed verification. + final AtomicInteger verifiedCount = new AtomicInteger(0); + // Error messages for verification failures. + final Deque failed = new ConcurrentLinkedDeque<>(); + + private VerifyJimage(List 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 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 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 BOOT_RESOURCES = Arrays.asList( - "java.base/META-INF/services/java.nio.file.spi.FileSystemProvider" - ); - private final List EXT_RESOURCES = Arrays.asList( - "jdk.zipfs/META-INF/services/java.nio.file.spi.FileSystemProvider" - ); - private final List 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 moduleEntries; + private final Set 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 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 EXCLUDED_MODULES = Set.of("jdk.graal.compiler"); + /** + * Predicate for files which correspond to entries in the jimage. + * + *

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. + * + *

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. + * + *

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 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 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); + } } }