< 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 -DDISABLE_PREVIEW_PATCHING=true ImageReaderTest
 69  */
 70 
 71 /// Using PER_CLASS lifecycle means the (expensive) image file is only build 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     public void testModuleDirectories_expected(String name) throws IOException {
107         try (ImageReader reader = ImageReader.open(image, PreviewMode.DISABLED)) {
108             assertDir(reader, name);
109         }
110     }
111 
112     @ParameterizedTest
113     @ValueSource(strings = {
114             "",
115             "//",
116             "/modules/",
117             "/modules/unknown",
118             "/modules/modbar/",
119             "/modules/modfoo//com",
120             "/modules/modfoo/com/"})
121     public void testModuleNodes_absent(String name) throws IOException {
122         try (ImageReader reader = ImageReader.open(image, PreviewMode.DISABLED)) {
123             assertAbsent(reader, name);
124         }
125     }
126 
127     @Test
128     public void testModuleResources() throws IOException {
129         try (ImageReader reader = ImageReader.open(image, PreviewMode.DISABLED)) {
130             assertNode(reader, "/modules/modfoo/com/foo/HasPreviewVersion.class");
131             assertNode(reader, "/modules/modbar/com/bar/One.class");
132 
133             ImageClassLoader loader = new ImageClassLoader(reader, IMAGE_ENTRIES.keySet());
134             assertNonPreviewVersion(loader, "modfoo", "com.foo.HasPreviewVersion");
135             assertNonPreviewVersion(loader, "modfoo", "com.foo.NormalFoo");
136             assertNonPreviewVersion(loader, "modfoo", "com.foo.bar.NormalBar");
137             assertNonPreviewVersion(loader, "modbar", "com.bar.One");
138         }
139     }
140 
141     @ParameterizedTest
142     @CsvSource(delimiter = ':', value = {
143             "modfoo:com/foo/HasPreviewVersion.class",
144             "modbar:com/bar/One.class",
145     })
146     public void testResource_present(String modName, String resPath) throws IOException {
147         try (ImageReader reader = ImageReader.open(image, PreviewMode.DISABLED)) {
148             assertNotNull(reader.findResourceNode(modName, resPath));
149             assertTrue(reader.containsResource(modName, resPath));
150 
151             String canonicalNodeName = "/modules/" + modName + "/" + resPath;
152             Node node = reader.findNode(canonicalNodeName);
153             assertTrue(node != null && node.isResource());
154         }
155     }
156 
157     @ParameterizedTest
158     @CsvSource(delimiter = ':', value = {
159             // Absolute resource names are not allowed.
160             "modfoo:/com/bar/One.class",
161             // Resource in wrong module.
162             "modfoo:com/bar/One.class",
163             "modbar:com/foo/HasPreviewVersion.class",
164             // Directories are not returned.
165             "modfoo:com/foo",
166             "modbar:com/bar",
167             // JImage entries exist for these, but they are not resources.
168             "modules:modfoo/com/foo/HasPreviewVersion.class",
169             "packages:com.foo/modfoo",
170             // Empty module names/paths do not find resources.
171             "'':modfoo/com/foo/HasPreviewVersion.class",
172             "modfoo:''"})
173     public void testResource_absent(String modName, String resPath) throws IOException {
174         try (ImageReader reader = ImageReader.open(image, PreviewMode.DISABLED)) {
175             assertNull(reader.findResourceNode(modName, resPath));
176             assertFalse(reader.containsResource(modName, resPath));
177 
178             // Non-existent resources names should either not be found,
179             // or (in the case of directory nodes) not be resources.
180             String canonicalNodeName = "/modules/" + modName + "/" + resPath;
181             Node node = reader.findNode(canonicalNodeName);
182             assertTrue(node == null || !node.isResource());
183         }
184     }
185 
186     @ParameterizedTest
187     @CsvSource(delimiter = ':', value = {
188             // Don't permit module names to contain paths.
189             "modfoo/com/bar:One.class",
190             "modfoo/com:bar/One.class",
191             "modules/modfoo/com:foo/HasPreviewVersion.class",
192     })
193     public void testResource_invalid(String modName, String resPath) throws IOException {
194         try (ImageReader reader = ImageReader.open(image, PreviewMode.DISABLED)) {
195             assertThrows(IllegalArgumentException.class, () -> reader.containsResource(modName, resPath));
196             assertThrows(IllegalArgumentException.class, () -> reader.findResourceNode(modName, resPath));
197         }
198     }
199 
200     @Test
201     public void testPackageDirectories() throws IOException {
202         try (ImageReader reader = ImageReader.open(image, PreviewMode.DISABLED)) {
203             Node root = assertDir(reader, "/packages");
204             Set<String> pkgNames = root.getChildNames().collect(toSet());
205             assertTrue(pkgNames.contains("/packages/com"));
206             assertTrue(pkgNames.contains("/packages/com.foo"));
207             assertTrue(pkgNames.contains("/packages/com.bar"));
208 
209             // Even though no classes exist directly in the "com" package, it still
210             // creates a directory with links back to all the modules which contain it.
211             Set<String> comLinks = assertDir(reader, "/packages/com").getChildNames().collect(Collectors.toSet());
212             assertTrue(comLinks.contains("/packages/com/modfoo"));
213             assertTrue(comLinks.contains("/packages/com/modbar"));
214         }
215     }
216 
217     @Test
218     public void testPackageLinks() throws IOException {
219         try (ImageReader reader = ImageReader.open(image, PreviewMode.DISABLED)) {
220             Node moduleFoo = assertDir(reader, "/modules/modfoo");
221             Node moduleBar = assertDir(reader, "/modules/modbar");
222             assertSame(assertLink(reader, "/packages/com.foo/modfoo").resolveLink(), moduleFoo);
223             assertSame(assertLink(reader, "/packages/com.bar/modbar").resolveLink(), moduleBar);
224         }
225     }
226 
227     @Test
228     public void testPreviewResources_disabled() throws IOException {
229         try (ImageReader reader = ImageReader.open(image, PreviewMode.DISABLED)) {
230             ImageClassLoader loader = new ImageClassLoader(reader, IMAGE_ENTRIES.keySet());
231 
232             // No preview classes visible.
233             assertNonPreviewVersion(loader, "modfoo", "com.foo.HasPreviewVersion");
234             assertNonPreviewVersion(loader, "modfoo", "com.foo.NormalFoo");
235             assertNonPreviewVersion(loader, "modfoo", "com.foo.bar.NormalBar");
236 
237             // NormalBar exists but IsPreviewOnly doesn't.
238             assertResource(reader, "modfoo", "com/foo/bar/NormalBar.class");
239             assertAbsent(reader, "/modules/modfoo/com/foo/bar/IsPreviewOnly.class");
240             assertDirContents(reader, "/modules/modfoo/com/foo", "HasPreviewVersion.class", "NormalFoo.class", "bar");
241             assertDirContents(reader, "/modules/modfoo/com/foo/bar", "NormalBar.class");
242         }
243     }
244 
245     @Test
246     public void testPreviewResources_enabled() throws IOException {
247         try (ImageReader reader = ImageReader.open(image, PreviewMode.ENABLED)) {
248             ImageClassLoader loader = new ImageClassLoader(reader, IMAGE_ENTRIES.keySet());
249 
250             // Preview version of classes either overwrite existing entries or are added to directories.
251             assertPreviewVersion(loader, "modfoo", "com.foo.HasPreviewVersion");
252             assertNonPreviewVersion(loader, "modfoo", "com.foo.NormalFoo");
253             assertNonPreviewVersion(loader, "modfoo", "com.foo.bar.NormalBar");
254             assertPreviewVersion(loader, "modfoo", "com.foo.bar.IsPreviewOnly");
255 
256             // Both NormalBar and IsPreviewOnly exist (direct lookup and as child nodes).
257             assertResource(reader, "modfoo", "com/foo/bar/NormalBar.class");
258             assertResource(reader, "modfoo", "com/foo/bar/IsPreviewOnly.class");
259             assertDirContents(reader, "/modules/modfoo/com/foo", "HasPreviewVersion.class", "NormalFoo.class", "bar");
260             assertDirContents(reader, "/modules/modfoo/com/foo/bar", "NormalBar.class", "IsPreviewOnly.class");
261         }
262     }
263 
264     @Test
265     public void testPreviewOnlyPackages_disabled() throws IOException {
266         try (ImageReader reader = ImageReader.open(image, PreviewMode.DISABLED)) {
267             ImageClassLoader loader = new ImageClassLoader(reader, IMAGE_ENTRIES.keySet());
268 
269             // No 'preview' package or anything inside it.
270             assertDirContents(reader, "/modules/modbar/com/bar", "One.class", "Two.class");
271             assertAbsent(reader, "/modules/modbar/com/bar/preview");
272             assertAbsent(reader, "/modules/modbar/com/bar/preview/stuff/Foo.class");
273 
274             // And no package link.
275             assertAbsent(reader, "/packages/com.bar.preview");
276         }
277     }
278 
279     @Test
280     public void testPreviewOnlyPackages_enabled() throws IOException {
281         try (ImageReader reader = ImageReader.open(image, PreviewMode.ENABLED)) {
282             ImageClassLoader loader = new ImageClassLoader(reader, IMAGE_ENTRIES.keySet());
283 
284             // In preview mode 'preview' package exists with preview only content.
285             assertDirContents(reader, "/modules/modbar/com/bar", "One.class", "Two.class", "preview");
286             assertDirContents(reader, "/modules/modbar/com/bar/preview/stuff", "Foo.class", "Bar.class");
287             assertResource(reader, "modbar", "com/bar/preview/stuff/Foo.class");
288 
289             // And package links exists.
290             assertDirContents(reader, "/packages/com.bar.preview", "modbar", "modgus");
291         }
292     }
293 
294     @Test
295     public void testPreviewModeLinks_disabled() throws IOException {
296         try (ImageReader reader = ImageReader.open(image, PreviewMode.DISABLED)) {
297             assertDirContents(reader, "/packages/com.bar", "modbar");
298             // Missing symbolic link and directory when not in preview mode.
299             assertAbsent(reader, "/packages/com.bar.preview");
300             assertAbsent(reader, "/packages/com.bar.preview.stuff");
301             assertAbsent(reader, "/modules/modbar/com/bar/preview");
302             assertAbsent(reader, "/modules/modbar/com/bar/preview/stuff");
303         }
304     }
305 
306     @Test
307     public void testPreviewModeLinks_enabled() throws IOException {
308         try (ImageReader reader = ImageReader.open(image, PreviewMode.ENABLED)) {
309             // In preview mode there is a new preview-only module visible.
310             assertDirContents(reader, "/packages/com.bar", "modbar", "modgus");
311             // And additional packages are present.
312             assertDirContents(reader, "/packages/com.bar.preview", "modbar", "modgus");
313             assertDirContents(reader, "/packages/com.bar.preview.stuff", "modbar");
314             assertDirContents(reader, "/packages/com.bar.preview.other", "modgus");
315             // And the preview-only content appears as we expect.
316             assertDirContents(reader, "/modules/modbar/com/bar", "One.class", "Two.class", "preview");
317             assertDirContents(reader, "/modules/modbar/com/bar/preview", "stuff");
318             assertDirContents(reader, "/modules/modbar/com/bar/preview/stuff", "Foo.class", "Bar.class");
319             // In both modules in which it was added.
320             assertDirContents(reader, "/modules/modgus/com/bar", "preview");
321             assertDirContents(reader, "/modules/modgus/com/bar/preview", "other");
322             assertDirContents(reader, "/modules/modgus/com/bar/preview/other", "Gus.class");
323         }
324     }
325 
326     @ParameterizedTest
327     @ValueSource(booleans = {false, true})
328     public void testPreviewEntriesAlwaysHidden(boolean previewMode) throws IOException {
329         try (ImageReader reader = ImageReader.open(image, previewMode ? PreviewMode.ENABLED : PreviewMode.DISABLED)) {
330             // The META-INF directory exists, but does not contain the preview directory.
331             Node dir = assertDir(reader, "/modules/modfoo/META-INF");
332             assertEquals(0, dir.getChildNames().filter(n -> n.endsWith("/preview")).count());
333             // Neither the preview directory, nor anything in it, can be looked-up directly.
334             assertAbsent(reader, "/modules/modfoo/META-INF/preview");
335             assertAbsent(reader, "/modules/modfoo/META-INF/preview/com/foo");
336             // HasPreviewVersion.class is a preview class in the test data, and thus appears in
337             // two places in the jimage). Ensure the preview version is always hidden.
338             String previewPath = "com/foo/HasPreviewVersion.class";
339             assertNode(reader, "/modules/modfoo/" + previewPath);
340             assertAbsent(reader, "/modules/modfoo/META-INF/preview/" + previewPath);
341         }
342     }
343 
344     private static ImageReader.Node assertNode(ImageReader reader, String name) throws IOException {
345         ImageReader.Node node = reader.findNode(name);
346         assertNotNull(node, "Could not find node: " + name);
347         return node;
348     }
349 
350     private static ImageReader.Node assertDir(ImageReader reader, String name) throws IOException {
351         ImageReader.Node dir = assertNode(reader, name);
352         assertTrue(dir.isDirectory(), "Node was not a directory: " + name);
353         return dir;
354     }
355 
356     private static void assertDirContents(ImageReader reader, String name, String... expectedChildNames) throws IOException {
357         Node dir = assertDir(reader, name);
358         Set<String> localChildNames = dir.getChildNames()
359                 .peek(s -> assertTrue(s.startsWith(name + "/")))
360                 .map(s -> s.substring(name.length() + 1))
361                 .collect(toSet());
362         assertEquals(
363                 Set.of(expectedChildNames),
364                 localChildNames,
365                 String.format("Unexpected child names in directory '%s'", name));
366     }
367 
368     private static void assertResource(ImageReader reader, String modName, String resPath) throws IOException {
369         assertTrue(reader.containsResource(modName, resPath), "Resource should exist: " + modName + "/" + resPath);
370         Node resNode = reader.findResourceNode(modName, resPath);
371         assertTrue(resNode.isResource(), "Node should be a resource: " + resNode.getName());
372         String nodeName = "/modules/" + modName + "/" + resPath;
373         assertEquals(nodeName, resNode.getName());
374         assertSame(resNode, reader.findNode(nodeName));
375     }
376 
377     private static void assertNonPreviewVersion(ImageClassLoader loader, String module, String fqn) throws IOException {
378         assertEquals("Class: " + fqn, loader.loadAndGetToString(module, fqn));
379     }
380 
381     private static void assertPreviewVersion(ImageClassLoader loader, String module, String fqn) throws IOException {
382         assertEquals("Preview: " + fqn, loader.loadAndGetToString(module, fqn));
383     }
384 
385     private static ImageReader.Node assertLink(ImageReader reader, String name) throws IOException {
386         ImageReader.Node link = assertNode(reader, name);
387         assertTrue(link.isLink(), "Node should be a symbolic link: " + link.getName());
388         return link;
389     }
390 
391     private static void assertAbsent(ImageReader reader, String name) throws IOException {
392         assertNull(reader.findNode(name), "Should not be able to find node: " + name);
393     }
394 
395     /// Builds a jimage file with the specified class entries. The classes in the built
396     /// image can be loaded and executed to return their names via `toString()` to confirm
397     /// the correct bytes were returned.
398     public static Path buildJImage(Map<String, List<String>> entries) {
399         Helper helper = getHelper();
400         Path outDir = helper.createNewImageDir("test");
401         JImageGenerator.JLinkTask jlink = JImageGenerator.getJLinkTask()
402                 .modulePath(helper.defaultModulePath())
403                 .output(outDir);
404 
405         Path jarDir = helper.getJarDir();
406         entries.forEach((module, classes) -> {
407             JarBuilder jar = new JarBuilder(jarDir.resolve(module + ".jar").toString());
408             String moduleInfo = "module " + module + " {}";
409             jar.addEntry("module-info.class", InMemoryJavaCompiler.compile("module-info", moduleInfo));
410 
411             classes.forEach(fqn -> {
412                 boolean isPreviewEntry = fqn.startsWith("@");
413                 if (isPreviewEntry) {
414                     fqn = fqn.substring(1);
415                 }
416                 int lastDot = fqn.lastIndexOf('.');
417                 String pkg = fqn.substring(0, lastDot);
418                 String cls = fqn.substring(lastDot + 1);


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