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