< prev index next >

test/jdk/jdk/internal/jimage/ImageReaderTest.java

Print this page

  6  * under the terms of the GNU General Public License version 2 only, as
  7  * published by the Free Software Foundation.
  8  *
  9  * This code is distributed in the hope that it will be useful, but WITHOUT
 10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 12  * version 2 for more details (a copy is included in the LICENSE file that
 13  * accompanied this code).
 14  *
 15  * You should have received a copy of the GNU General Public License version
 16  * 2 along with this work; if not, write to the Free Software Foundation,
 17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 18  *
 19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 20  * or visit www.oracle.com if you need additional information or have any
 21  * questions.
 22  */
 23 
 24 import jdk.internal.jimage.ImageReader;
 25 import jdk.internal.jimage.ImageReader.Node;

 26 import jdk.test.lib.compiler.InMemoryJavaCompiler;
 27 import jdk.test.lib.util.JarBuilder;
 28 import jdk.tools.jlink.internal.LinkableRuntimeImage;
 29 import org.junit.jupiter.api.Assumptions;
 30 import org.junit.jupiter.api.Test;
 31 import org.junit.jupiter.api.TestInstance;
 32 import org.junit.jupiter.params.ParameterizedTest;
 33 import org.junit.jupiter.params.provider.CsvSource;
 34 import org.junit.jupiter.params.provider.ValueSource;
 35 import tests.Helper;
 36 import tests.JImageGenerator;
 37 
 38 import java.io.IOException;
 39 import java.nio.file.Path;
 40 import java.util.Arrays;
 41 import java.util.List;
 42 import java.util.Map;
 43 import java.util.Set;
 44 import java.util.stream.Collectors;
 45 

 46 import static org.junit.jupiter.api.Assertions.assertEquals;
 47 import static org.junit.jupiter.api.Assertions.assertFalse;
 48 import static org.junit.jupiter.api.Assertions.assertNotNull;
 49 import static org.junit.jupiter.api.Assertions.assertNull;
 50 import static org.junit.jupiter.api.Assertions.assertSame;
 51 import static org.junit.jupiter.api.Assertions.assertThrows;
 52 import static org.junit.jupiter.api.Assertions.assertTrue;
 53 import static org.junit.jupiter.api.Assertions.fail;
 54 import static org.junit.jupiter.api.Assumptions.assumeTrue;
 55 import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS;
 56 
 57 /*
 58  * @test
 59  * @summary Tests for ImageReader.
 60  * @modules java.base/jdk.internal.jimage
 61  *          jdk.jlink/jdk.tools.jlink.internal
 62  *          jdk.jlink/jdk.tools.jimage
 63  * @library /test/jdk/tools/lib
 64  *          /test/lib
 65  * @build tests.*
 66  * @run junit/othervm ImageReaderTest
 67  */
 68 
 69 /// Using PER_CLASS lifecycle means the (expensive) image file is only build once.
 70 /// There is no mutable test instance state to worry about.
 71 @TestInstance(PER_CLASS)
 72 public class ImageReaderTest {
 73 

 74     private static final Map<String, List<String>> IMAGE_ENTRIES = Map.of(
 75             "modfoo", Arrays.asList(
 76                     "com.foo.Alpha",
 77                     "com.foo.Beta",
 78                     "com.foo.bar.Gamma"),




 79             "modbar", Arrays.asList(
 80                     "com.bar.One",
 81                     "com.bar.Two"));






 82     private final Path image = buildJImage(IMAGE_ENTRIES);
 83 
 84     @ParameterizedTest
 85     @ValueSource(strings = {
 86             "/",
 87             "/modules",
 88             "/modules/modfoo",
 89             "/modules/modbar",
 90             "/modules/modfoo/com",
 91             "/modules/modfoo/com/foo",
 92             "/modules/modfoo/com/foo/bar"})

 93     public void testModuleDirectories_expected(String name) throws IOException {
 94         try (ImageReader reader = ImageReader.open(image)) {
 95             assertDir(reader, name);
 96         }
 97     }
 98 
 99     @ParameterizedTest
100     @ValueSource(strings = {
101             "",
102             "//",
103             "/modules/",
104             "/modules/unknown",
105             "/modules/modbar/",
106             "/modules/modfoo//com",
107             "/modules/modfoo/com/"})

108     public void testModuleNodes_absent(String name) throws IOException {
109         try (ImageReader reader = ImageReader.open(image)) {
110             assertAbsent(reader, name);
111         }
112     }
113 
114     @Test
115     public void testModuleResources() throws IOException {
116         try (ImageReader reader = ImageReader.open(image)) {
117             assertNode(reader, "/modules/modfoo/com/foo/Alpha.class");
118             assertNode(reader, "/modules/modbar/com/bar/One.class");
119 
120             ImageClassLoader loader = new ImageClassLoader(reader, IMAGE_ENTRIES.keySet());
121             assertEquals("Class: com.foo.Alpha", loader.loadAndGetToString("modfoo", "com.foo.Alpha"));
122             assertEquals("Class: com.foo.Beta", loader.loadAndGetToString("modfoo", "com.foo.Beta"));
123             assertEquals("Class: com.foo.bar.Gamma", loader.loadAndGetToString("modfoo", "com.foo.bar.Gamma"));
124             assertEquals("Class: com.bar.One", loader.loadAndGetToString("modbar", "com.bar.One"));
125         }
126     }
127 
128     @ParameterizedTest
129     @CsvSource(delimiter = ':', value = {
130             "modfoo:com/foo/Alpha.class",
131             "modbar:com/bar/One.class",
132     })
133     public void testResource_present(String modName, String resPath) throws IOException {
134         try (ImageReader reader = ImageReader.open(image)) {
135             assertNotNull(reader.findResourceNode(modName, resPath));
136             assertTrue(reader.containsResource(modName, resPath));

137 
138             String canonicalNodeName = "/modules/" + modName + "/" + resPath;
139             Node node = reader.findNode(canonicalNodeName);
140             assertTrue(node != null && node.isResource());

141         }
142     }
143 
144     @ParameterizedTest
145     @CsvSource(delimiter = ':', value = {
146             // Absolute resource names are not allowed.
147             "modfoo:/com/bar/One.class",
148             // Resource in wrong module.
149             "modfoo:com/bar/One.class",
150             "modbar:com/foo/Alpha.class",
151             // Directories are not returned.
152             "modfoo:com/foo",
153             "modbar:com/bar",
154             // JImage entries exist for these, but they are not resources.
155             "modules:modfoo/com/foo/Alpha.class",
156             "packages:com.foo/modfoo",
157             // Empty module names/paths do not find resources.
158             "'':modfoo/com/foo/Alpha.class",
159             "modfoo:''"})



160     public void testResource_absent(String modName, String resPath) throws IOException {
161         try (ImageReader reader = ImageReader.open(image)) {
162             assertNull(reader.findResourceNode(modName, resPath));
163             assertFalse(reader.containsResource(modName, resPath));
164 
165             // Non-existent resources names should either not be found,
166             // or (in the case of directory nodes) not be resources.
167             String canonicalNodeName = "/modules/" + modName + "/" + resPath;
168             Node node = reader.findNode(canonicalNodeName);
169             assertTrue(node == null || !node.isResource());


170         }
171     }
172 
173     @ParameterizedTest
174     @CsvSource(delimiter = ':', value = {
175             // Don't permit module names to contain paths.
176             "modfoo/com/bar:One.class",
177             "modfoo/com:bar/One.class",
178             "modules/modfoo/com:foo/Alpha.class",
179     })
180     public void testResource_invalid(String modName, String resPath) throws IOException {
181         try (ImageReader reader = ImageReader.open(image)) {
182             assertThrows(IllegalArgumentException.class, () -> reader.containsResource(modName, resPath));
183             assertThrows(IllegalArgumentException.class, () -> reader.findResourceNode(modName, resPath));


184         }
185     }
186 
187     @Test
188     public void testPackageDirectories() throws IOException {
189         try (ImageReader reader = ImageReader.open(image)) {
190             Node root = assertDir(reader, "/packages");
191             Set<String> pkgNames = root.getChildNames().collect(Collectors.toSet());
192             assertTrue(pkgNames.contains("/packages/com"));
193             assertTrue(pkgNames.contains("/packages/com.foo"));
194             assertTrue(pkgNames.contains("/packages/com.bar"));
195 
196             // Even though no classes exist directly in the "com" package, it still
197             // creates a directory with links back to all the modules which contain it.
198             Set<String> comLinks = assertDir(reader, "/packages/com").getChildNames().collect(Collectors.toSet());
199             assertTrue(comLinks.contains("/packages/com/modfoo"));
200             assertTrue(comLinks.contains("/packages/com/modbar"));
201         }
202     }
203 
204     @Test
205     public void testPackageLinks() throws IOException {
206         try (ImageReader reader = ImageReader.open(image)) {
207             Node moduleFoo = assertDir(reader, "/modules/modfoo");
208             Node moduleBar = assertDir(reader, "/modules/modbar");
209             assertSame(assertLink(reader, "/packages/com.foo/modfoo").resolveLink(), moduleFoo);
210             assertSame(assertLink(reader, "/packages/com.bar/modbar").resolveLink(), moduleBar);
211         }
212     }
213 





















































































































214     private static ImageReader.Node assertNode(ImageReader reader, String name) throws IOException {
215         ImageReader.Node node = reader.findNode(name);
216         assertNotNull(node, "Could not find node: " + name);
217         return node;
218     }
219 
220     private static ImageReader.Node assertDir(ImageReader reader, String name) throws IOException {
221         ImageReader.Node dir = assertNode(reader, name);
222         assertTrue(dir.isDirectory(), "Node was not a directory: " + name);
223         return dir;
224     }
225 





























226     private static ImageReader.Node assertLink(ImageReader reader, String name) throws IOException {
227         ImageReader.Node link = assertNode(reader, name);
228         assertTrue(link.isLink(), "Node was not a symbolic link: " + name);
229         return link;
230     }
231 
232     private static void assertAbsent(ImageReader reader, String name) throws IOException {
233         assertNull(reader.findNode(name), "Should not be able to find node: " + name);
234     }
235 
236     /// Builds a jimage file with the specified class entries. The classes in the built
237     /// image can be loaded and executed to return their names via `toString()` to confirm
238     /// the correct bytes were returned.
239     public static Path buildJImage(Map<String, List<String>> entries) {
240         Helper helper = getHelper();
241         Path outDir = helper.createNewImageDir("test");
242         JImageGenerator.JLinkTask jlink = JImageGenerator.getJLinkTask()
243                 .modulePath(helper.defaultModulePath())
244                 .output(outDir);
245 
246         Path jarDir = helper.getJarDir();
247         entries.forEach((module, classes) -> {
248             JarBuilder jar = new JarBuilder(jarDir.resolve(module + ".jar").toString());
249             String moduleInfo = "module " + module + " {}";
250             jar.addEntry("module-info.class", InMemoryJavaCompiler.compile("module-info", moduleInfo));
251 
252             classes.forEach(fqn -> {




253                 int lastDot = fqn.lastIndexOf('.');
254                 String pkg = fqn.substring(0, lastDot);
255                 String cls = fqn.substring(lastDot + 1);
256 
257                 String path = fqn.replace('.', '/') + ".class";
258                 String source = String.format(
259                         """
260                         package %s;
261                         public class %s {
262                             public String toString() {
263                                 return "Class: %s";
264                             }
265                         }
266                         """, pkg, cls, fqn);

267                 jar.addEntry(path, InMemoryJavaCompiler.compile(fqn, source));
268             });
269             try {
270                 jar.build();
271             } catch (IOException e) {
272                 throw new RuntimeException(e);
273             }
274             jlink.addMods(module);
275         });
276         return jlink.call().assertSuccess().resolve("lib", "modules");
277     }
278 
279     ///  Returns the helper for building JAR and jimage files.
280     private static Helper getHelper() {
281         Helper helper;
282         try {
283             boolean isLinkableRuntime = LinkableRuntimeImage.isLinkableRuntime();
284             helper = Helper.newHelper(isLinkableRuntime);
285         } catch (IOException e) {
286             throw new RuntimeException(e);

  6  * under the terms of the GNU General Public License version 2 only, as
  7  * published by the Free Software Foundation.
  8  *
  9  * This code is distributed in the hope that it will be useful, but WITHOUT
 10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 12  * version 2 for more details (a copy is included in the LICENSE file that
 13  * accompanied this code).
 14  *
 15  * You should have received a copy of the GNU General Public License version
 16  * 2 along with this work; if not, write to the Free Software Foundation,
 17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 18  *
 19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 20  * or visit www.oracle.com if you need additional information or have any
 21  * questions.
 22  */
 23 
 24 import jdk.internal.jimage.ImageReader;
 25 import jdk.internal.jimage.ImageReader.Node;
 26 import jdk.internal.jimage.PreviewMode;
 27 import jdk.test.lib.compiler.InMemoryJavaCompiler;
 28 import jdk.test.lib.util.JarBuilder;
 29 import jdk.tools.jlink.internal.LinkableRuntimeImage;
 30 import org.junit.jupiter.api.Assumptions;
 31 import org.junit.jupiter.api.Test;
 32 import org.junit.jupiter.api.TestInstance;
 33 import org.junit.jupiter.params.ParameterizedTest;
 34 import org.junit.jupiter.params.provider.CsvSource;
 35 import org.junit.jupiter.params.provider.ValueSource;
 36 import tests.Helper;
 37 import tests.JImageGenerator;
 38 
 39 import java.io.IOException;
 40 import java.nio.file.Path;
 41 import java.util.Arrays;
 42 import java.util.List;
 43 import java.util.Map;
 44 import java.util.Set;
 45 import java.util.stream.Collectors;
 46 
 47 import static java.util.stream.Collectors.toSet;
 48 import static org.junit.jupiter.api.Assertions.assertEquals;
 49 import static org.junit.jupiter.api.Assertions.assertFalse;
 50 import static org.junit.jupiter.api.Assertions.assertNotNull;
 51 import static org.junit.jupiter.api.Assertions.assertNull;
 52 import static org.junit.jupiter.api.Assertions.assertSame;
 53 import static org.junit.jupiter.api.Assertions.assertThrows;
 54 import static org.junit.jupiter.api.Assertions.assertTrue;
 55 import static org.junit.jupiter.api.Assertions.fail;
 56 import static org.junit.jupiter.api.Assumptions.assumeTrue;
 57 import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS;
 58 
 59 /*
 60  * @test
 61  * @summary Tests for ImageReader.
 62  * @modules java.base/jdk.internal.jimage
 63  *          jdk.jlink/jdk.tools.jlink.internal
 64  *          jdk.jlink/jdk.tools.jimage
 65  * @library /test/jdk/tools/lib
 66  *          /test/lib
 67  * @build tests.*
 68  * @run junit/othervm -esa ImageReaderTest
 69  */
 70 
 71 /// Using PER_CLASS lifecycle means the (expensive) image file is only built once.
 72 /// There is no mutable test instance state to worry about.
 73 @TestInstance(PER_CLASS)
 74 public class ImageReaderTest {
 75     // The '@' prefix marks the entry as a preview entry which will be placed in
 76     // the '/modules/<module>/META-INF/preview/...' namespace.
 77     private static final Map<String, List<String>> IMAGE_ENTRIES = Map.of(
 78             "modfoo", Arrays.asList(
 79                     "com.foo.HasPreviewVersion",
 80                     "com.foo.NormalFoo",
 81                     "com.foo.bar.NormalBar",
 82                     // Replaces original class in preview mode.
 83                     "@com.foo.HasPreviewVersion",
 84                     // New class in existing package in preview mode.
 85                     "@com.foo.bar.IsPreviewOnly"),
 86             "modbar", Arrays.asList(
 87                     "com.bar.One",
 88                     "com.bar.Two",
 89                     // Two new packages in preview mode (new symbolic links).
 90                     "@com.bar.preview.stuff.Foo",
 91                     "@com.bar.preview.stuff.Bar"),
 92             "modgus", Arrays.asList(
 93                     // A second module with a preview-only empty package (preview).
 94                     "@com.bar.preview.other.Gus"));
 95     private final Path image = buildJImage(IMAGE_ENTRIES);
 96 
 97     @ParameterizedTest
 98     @ValueSource(strings = {
 99             "/",
100             "/modules",
101             "/modules/modfoo",
102             "/modules/modbar",
103             "/modules/modfoo/com",
104             "/modules/modfoo/com/foo",
105             "/modules/modfoo/com/foo/bar",
106     })
107     public void testModuleDirectories_expected(String name) throws IOException {
108         try (ImageReader reader = ImageReader.open(image, PreviewMode.DISABLED)) {
109             assertDir(reader, name);
110         }
111     }
112 
113     @ParameterizedTest
114     @ValueSource(strings = {
115             "",
116             "//",
117             "/modules/",
118             "/modules/unknown",
119             "/modules/modbar/",
120             "/modules/modfoo//com",
121             "/modules/modfoo/com/",
122     })
123     public void testModuleNodes_absent(String name) throws IOException {
124         try (ImageReader reader = ImageReader.open(image, PreviewMode.DISABLED)) {
125             assertAbsent(reader, name);
126         }
127     }
128 
129     @Test
130     public void testModuleResources() throws IOException {
131         try (ImageReader reader = ImageReader.open(image, PreviewMode.DISABLED)) {
132             assertNode(reader, "/modules/modfoo/com/foo/HasPreviewVersion.class");
133             assertNode(reader, "/modules/modbar/com/bar/One.class");
134 
135             ImageClassLoader loader = new ImageClassLoader(reader, IMAGE_ENTRIES.keySet());
136             assertNonPreviewVersion(loader, "modfoo", "com.foo.HasPreviewVersion");
137             assertNonPreviewVersion(loader, "modfoo", "com.foo.NormalFoo");
138             assertNonPreviewVersion(loader, "modfoo", "com.foo.bar.NormalBar");
139             assertNonPreviewVersion(loader, "modbar", "com.bar.One");
140         }
141     }
142 
143     @ParameterizedTest
144     @CsvSource(delimiter = ':', value = {
145             "modfoo:com/foo/HasPreviewVersion.class",
146             "modbar:com/bar/One.class",
147     })
148     public void testResource_present(String modName, String resPath) throws IOException {
149         for (PreviewMode mode : List.of(PreviewMode.ENABLED, PreviewMode.DISABLED)) {
150             try (ImageReader reader = ImageReader.open(image, mode)) {
151                 assertNotNull(reader.findResourceNode(modName, resPath));
152                 assertTrue(reader.containsResource(modName, resPath));
153 
154                 String canonicalNodeName = "/modules/" + modName + "/" + resPath;
155                 Node node = reader.findNode(canonicalNodeName);
156                 assertTrue(node != null && node.isResource());
157             }
158         }
159     }
160 
161     @ParameterizedTest
162     @CsvSource(delimiter = ':', value = {
163             // Absolute resource names are not allowed.
164             "modfoo:/com/bar/One.class",
165             // Resource in wrong module.
166             "modfoo:com/bar/One.class",
167             "modbar:com/foo/HasPreviewVersion.class",
168             // Directories are not returned.
169             "modfoo:com/foo",
170             "modbar:com/bar",
171             // JImage entries exist for these, but they are not resources.
172             "modules:modfoo/com/foo/HasPreviewVersion.class",
173             "packages:com.foo/modfoo",
174             // Empty module names/paths do not find resources.
175             "'':modfoo/com/foo/HasPreviewVersion.class",
176             "modfoo:''",
177             // Make sure preview paths are excluded.
178             "modfoo:META-INF/preview/com/foo/HasPreviewVersion.class",
179     })
180     public void testResource_absent(String modName, String resPath) throws IOException {
181         for (PreviewMode mode : List.of(PreviewMode.ENABLED, PreviewMode.DISABLED)) {
182             try (ImageReader reader = ImageReader.open(image, mode)) {
183                 assertNull(reader.findResourceNode(modName, resPath));
184                 assertFalse(reader.containsResource(modName, resPath));
185 
186                 // Non-existent resources names should either not be found,
187                 // or (in the case of directory nodes) not be resources.
188                 String canonicalNodeName = "/modules/" + modName + "/" + resPath;
189                 Node node = reader.findNode(canonicalNodeName);
190                 assertTrue(node == null || !node.isResource());
191             }
192         }
193     }
194 
195     @ParameterizedTest
196     @CsvSource(delimiter = ':', value = {
197             // Don't permit module names to contain paths.
198             "modfoo/com/bar:One.class",
199             "modfoo/com:bar/One.class",
200             "modules/modfoo/com:foo/HasPreviewVersion.class",
201     })
202     public void testResource_invalid(String modName, String resPath) throws IOException {
203         for (PreviewMode mode : List.of(PreviewMode.ENABLED, PreviewMode.DISABLED)) {
204             try (ImageReader reader = ImageReader.open(image, mode)) {
205                 assertThrows(IllegalArgumentException.class, () -> reader.containsResource(modName, resPath));
206                 assertThrows(IllegalArgumentException.class, () -> reader.findResourceNode(modName, resPath));
207             }
208         }
209     }
210 
211     @Test
212     public void testPackageDirectories() throws IOException {
213         try (ImageReader reader = ImageReader.open(image, PreviewMode.DISABLED)) {
214             Node root = assertDir(reader, "/packages");
215             Set<String> pkgNames = root.getChildNames().collect(toSet());
216             assertTrue(pkgNames.contains("/packages/com"));
217             assertTrue(pkgNames.contains("/packages/com.foo"));
218             assertTrue(pkgNames.contains("/packages/com.bar"));
219 
220             // Even though no classes exist directly in the "com" package, it still
221             // creates a directory with links back to all the modules which contain it.
222             Set<String> comLinks = assertDir(reader, "/packages/com").getChildNames().collect(Collectors.toSet());
223             assertTrue(comLinks.contains("/packages/com/modfoo"));
224             assertTrue(comLinks.contains("/packages/com/modbar"));
225         }
226     }
227 
228     @Test
229     public void testPackageLinks() throws IOException {
230         try (ImageReader reader = ImageReader.open(image, PreviewMode.DISABLED)) {
231             Node moduleFoo = assertDir(reader, "/modules/modfoo");
232             Node moduleBar = assertDir(reader, "/modules/modbar");
233             assertSame(assertLink(reader, "/packages/com.foo/modfoo").resolveLink(), moduleFoo);
234             assertSame(assertLink(reader, "/packages/com.bar/modbar").resolveLink(), moduleBar);
235         }
236     }
237 
238     @Test
239     public void testPreviewResources_disabled() throws IOException {
240         try (ImageReader reader = ImageReader.open(image, PreviewMode.DISABLED)) {
241             ImageClassLoader loader = new ImageClassLoader(reader, IMAGE_ENTRIES.keySet());
242 
243             // No preview classes visible.
244             assertNonPreviewVersion(loader, "modfoo", "com.foo.HasPreviewVersion");
245             assertNonPreviewVersion(loader, "modfoo", "com.foo.NormalFoo");
246             assertNonPreviewVersion(loader, "modfoo", "com.foo.bar.NormalBar");
247 
248             // NormalBar exists but IsPreviewOnly doesn't.
249             assertResource(reader, "modfoo", "com/foo/bar/NormalBar.class");
250             assertAbsent(reader, "/modules/modfoo/com/foo/bar/IsPreviewOnly.class");
251             assertDirContents(reader, "/modules/modfoo/com/foo", "HasPreviewVersion.class", "NormalFoo.class", "bar");
252             assertDirContents(reader, "/modules/modfoo/com/foo/bar", "NormalBar.class");
253         }
254     }
255 
256     @Test
257     public void testPreviewResources_enabled() throws IOException {
258         try (ImageReader reader = ImageReader.open(image, PreviewMode.ENABLED)) {
259             ImageClassLoader loader = new ImageClassLoader(reader, IMAGE_ENTRIES.keySet());
260 
261             // Preview version of classes either overwrite existing entries or are added to directories.
262             assertPreviewVersion(loader, "modfoo", "com.foo.HasPreviewVersion");
263             assertNonPreviewVersion(loader, "modfoo", "com.foo.NormalFoo");
264             assertNonPreviewVersion(loader, "modfoo", "com.foo.bar.NormalBar");
265             assertPreviewVersion(loader, "modfoo", "com.foo.bar.IsPreviewOnly");
266 
267             // Both NormalBar and IsPreviewOnly exist (direct lookup and as child nodes).
268             assertResource(reader, "modfoo", "com/foo/bar/NormalBar.class");
269             assertResource(reader, "modfoo", "com/foo/bar/IsPreviewOnly.class");
270             assertDirContents(reader, "/modules/modfoo/com/foo", "HasPreviewVersion.class", "NormalFoo.class", "bar");
271             assertDirContents(reader, "/modules/modfoo/com/foo/bar", "NormalBar.class", "IsPreviewOnly.class");
272         }
273     }
274 
275     @Test
276     public void testPreviewOnlyPackages_disabled() throws IOException {
277         try (ImageReader reader = ImageReader.open(image, PreviewMode.DISABLED)) {
278             ImageClassLoader loader = new ImageClassLoader(reader, IMAGE_ENTRIES.keySet());
279 
280             // No 'preview' package or anything inside it.
281             assertDirContents(reader, "/modules/modbar/com/bar", "One.class", "Two.class");
282             assertAbsent(reader, "/modules/modbar/com/bar/preview");
283             assertAbsent(reader, "/modules/modbar/com/bar/preview/stuff/Foo.class");
284 
285             // And no package link.
286             assertAbsent(reader, "/packages/com.bar.preview");
287         }
288     }
289 
290     @Test
291     public void testPreviewOnlyPackages_enabled() throws IOException {
292         try (ImageReader reader = ImageReader.open(image, PreviewMode.ENABLED)) {
293             ImageClassLoader loader = new ImageClassLoader(reader, IMAGE_ENTRIES.keySet());
294 
295             // In preview mode 'preview' package exists with preview only content.
296             assertDirContents(reader, "/modules/modbar/com/bar", "One.class", "Two.class", "preview");
297             assertDirContents(reader, "/modules/modbar/com/bar/preview/stuff", "Foo.class", "Bar.class");
298             assertResource(reader, "modbar", "com/bar/preview/stuff/Foo.class");
299 
300             // And package links exists.
301             assertDirContents(reader, "/packages/com.bar.preview", "modbar", "modgus");
302         }
303     }
304 
305     @Test
306     public void testPreviewModeLinks_disabled() throws IOException {
307         try (ImageReader reader = ImageReader.open(image, PreviewMode.DISABLED)) {
308             assertDirContents(reader, "/packages/com.bar", "modbar");
309             // Missing symbolic link and directory when not in preview mode.
310             assertAbsent(reader, "/packages/com.bar.preview");
311             assertAbsent(reader, "/packages/com.bar.preview.stuff");
312             assertAbsent(reader, "/modules/modbar/com/bar/preview");
313             assertAbsent(reader, "/modules/modbar/com/bar/preview/stuff");
314         }
315     }
316 
317     @Test
318     public void testPreviewModeLinks_enabled() throws IOException {
319         try (ImageReader reader = ImageReader.open(image, PreviewMode.ENABLED)) {
320             // In preview mode there is a new preview-only module visible.
321             assertDirContents(reader, "/packages/com.bar", "modbar", "modgus");
322             // And additional packages are present.
323             assertDirContents(reader, "/packages/com.bar.preview", "modbar", "modgus");
324             assertDirContents(reader, "/packages/com.bar.preview.stuff", "modbar");
325             assertDirContents(reader, "/packages/com.bar.preview.other", "modgus");
326             // And the preview-only content appears as we expect.
327             assertDirContents(reader, "/modules/modbar/com/bar", "One.class", "Two.class", "preview");
328             assertDirContents(reader, "/modules/modbar/com/bar/preview", "stuff");
329             assertDirContents(reader, "/modules/modbar/com/bar/preview/stuff", "Foo.class", "Bar.class");
330             // In both modules in which it was added.
331             assertDirContents(reader, "/modules/modgus/com/bar", "preview");
332             assertDirContents(reader, "/modules/modgus/com/bar/preview", "other");
333             assertDirContents(reader, "/modules/modgus/com/bar/preview/other", "Gus.class");
334         }
335     }
336 
337     @ParameterizedTest
338     @ValueSource(booleans = {false, true})
339     public void testPreviewEntriesAlwaysHidden(boolean previewMode) throws IOException {
340         try (ImageReader reader = ImageReader.open(image, previewMode ? PreviewMode.ENABLED : PreviewMode.DISABLED)) {
341             // The META-INF directory exists, but does not contain the preview directory.
342             Node dir = assertDir(reader, "/modules/modfoo/META-INF");
343             assertEquals(0, dir.getChildNames().filter(n -> n.endsWith("/preview")).count());
344             // Neither the preview directory, nor anything in it, can be looked-up directly.
345             assertAbsent(reader, "/modules/modfoo/META-INF/preview");
346             assertAbsent(reader, "/modules/modfoo/META-INF/preview/com/foo");
347             // HasPreviewVersion.class is a preview class in the test data, and thus appears in
348             // two places in the jimage). Ensure the preview version is always hidden.
349             String previewPath = "com/foo/HasPreviewVersion.class";
350             assertNode(reader, "/modules/modfoo/" + previewPath);
351             assertAbsent(reader, "/modules/modfoo/META-INF/preview/" + previewPath);
352         }
353     }
354 
355     private static ImageReader.Node assertNode(ImageReader reader, String name) throws IOException {
356         ImageReader.Node node = reader.findNode(name);
357         assertNotNull(node, "Could not find node: " + name);
358         return node;
359     }
360 
361     private static ImageReader.Node assertDir(ImageReader reader, String name) throws IOException {
362         ImageReader.Node dir = assertNode(reader, name);
363         assertTrue(dir.isDirectory(), "Node was not a directory: " + name);
364         return dir;
365     }
366 
367     private static void assertDirContents(ImageReader reader, String name, String... expectedChildNames) throws IOException {
368         Node dir = assertDir(reader, name);
369         Set<String> localChildNames = dir.getChildNames()
370                 .peek(s -> assertTrue(s.startsWith(name + "/")))
371                 .map(s -> s.substring(name.length() + 1))
372                 .collect(toSet());
373         assertEquals(
374                 Set.of(expectedChildNames),
375                 localChildNames,
376                 String.format("Unexpected child names in directory '%s'", name));
377     }
378 
379     private static void assertResource(ImageReader reader, String modName, String resPath) throws IOException {
380         assertTrue(reader.containsResource(modName, resPath), "Resource should exist: " + modName + "/" + resPath);
381         Node resNode = reader.findResourceNode(modName, resPath);
382         assertTrue(resNode.isResource(), "Node should be a resource: " + resNode.getName());
383         String nodeName = "/modules/" + modName + "/" + resPath;
384         assertEquals(nodeName, resNode.getName());
385         assertSame(resNode, reader.findNode(nodeName));
386     }
387 
388     private static void assertNonPreviewVersion(ImageClassLoader loader, String module, String fqn) throws IOException {
389         assertEquals("Class: " + fqn, loader.loadAndGetToString(module, fqn));
390     }
391 
392     private static void assertPreviewVersion(ImageClassLoader loader, String module, String fqn) throws IOException {
393         assertEquals("Preview: " + fqn, loader.loadAndGetToString(module, fqn));
394     }
395 
396     private static ImageReader.Node assertLink(ImageReader reader, String name) throws IOException {
397         ImageReader.Node link = assertNode(reader, name);
398         assertTrue(link.isLink(), "Node should be a symbolic link: " + link.getName());
399         return link;
400     }
401 
402     private static void assertAbsent(ImageReader reader, String name) throws IOException {
403         assertNull(reader.findNode(name), "Should not be able to find node: " + name);
404     }
405 
406     /// Builds a jimage file with the specified class entries. The classes in the built
407     /// image can be loaded and executed to return their names via `toString()` to confirm
408     /// the correct bytes were returned.
409     public static Path buildJImage(Map<String, List<String>> entries) {
410         Helper helper = getHelper();
411         Path outDir = helper.createNewImageDir("test");
412         JImageGenerator.JLinkTask jlink = JImageGenerator.getJLinkTask()
413                 .modulePath(helper.defaultModulePath())
414                 .output(outDir);
415 
416         Path jarDir = helper.getJarDir();
417         entries.forEach((module, classes) -> {
418             JarBuilder jar = new JarBuilder(jarDir.resolve(module + ".jar").toString());
419             String moduleInfo = "module " + module + " {}";
420             jar.addEntry("module-info.class", InMemoryJavaCompiler.compile("module-info", moduleInfo));
421 
422             classes.forEach(fqn -> {
423                 boolean isPreviewEntry = fqn.startsWith("@");
424                 if (isPreviewEntry) {
425                     fqn = fqn.substring(1);
426                 }
427                 int lastDot = fqn.lastIndexOf('.');
428                 String pkg = fqn.substring(0, lastDot);
429                 String cls = fqn.substring(lastDot + 1);


430                 String source = String.format(
431                         """
432                         package %s;
433                         public class %s {
434                             public String toString() {
435                                 return "%s: %s";
436                             }
437                         }
438                         """, pkg, cls, isPreviewEntry ? "Preview" : "Class", fqn);
439                 String path = (isPreviewEntry ? "META-INF/preview/" : "") + fqn.replace('.', '/') + ".class";
440                 jar.addEntry(path, InMemoryJavaCompiler.compile(fqn, source));
441             });
442             try {
443                 jar.build();
444             } catch (IOException e) {
445                 throw new RuntimeException(e);
446             }
447             jlink.addMods(module);
448         });
449         return jlink.call().assertSuccess().resolve("lib", "modules");
450     }
451 
452     ///  Returns the helper for building JAR and jimage files.
453     private static Helper getHelper() {
454         Helper helper;
455         try {
456             boolean isLinkableRuntime = LinkableRuntimeImage.isLinkableRuntime();
457             helper = Helper.newHelper(isLinkableRuntime);
458         } catch (IOException e) {
459             throw new RuntimeException(e);
< prev index next >