1 /**
   2  * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   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 /*
  25  * @test
  26  * @library /lib/testlibrary
  27  * @modules jdk.compiler
  28  *          jdk.jlink
  29  * @build jdk.testlibrary.FileUtils CompilerUtils
  30  * @run testng JmodTest
  31  * @summary Basic test for jmod
  32  */
  33 
  34 import java.io.*;
  35 import java.lang.module.ModuleDescriptor;
  36 import java.lang.reflect.Method;
  37 import java.nio.file.*;
  38 import java.util.*;
  39 import java.util.function.Consumer;
  40 import java.util.regex.Pattern;
  41 import java.util.spi.ToolProvider;
  42 import java.util.stream.Stream;
  43 import jdk.testlibrary.FileUtils;
  44 import org.testng.annotations.BeforeTest;
  45 import org.testng.annotations.Test;
  46 
  47 import static java.io.File.pathSeparator;
  48 import static java.lang.module.ModuleDescriptor.Version;
  49 import static java.nio.charset.StandardCharsets.UTF_8;
  50 import static java.util.stream.Collectors.toSet;
  51 import static org.testng.Assert.*;
  52 
  53 public class JmodTest {
  54 
  55     static final ToolProvider JMOD_TOOL = ToolProvider.findFirst("jmod")
  56         .orElseThrow(() ->
  57             new RuntimeException("jmod tool not found")
  58         );
  59 
  60     static final String TEST_SRC = System.getProperty("test.src", ".");
  61     static final Path SRC_DIR = Paths.get(TEST_SRC, "src");
  62     static final Path EXPLODED_DIR = Paths.get("build");
  63     static final Path MODS_DIR = Paths.get("jmods");
  64 
  65     static final String CLASSES_PREFIX = "classes/";
  66     static final String CMDS_PREFIX = "bin/";
  67     static final String LIBS_PREFIX = "native/";
  68     static final String CONFIGS_PREFIX = "conf/";
  69 
  70     @BeforeTest
  71     public void buildExplodedModules() throws IOException {
  72         if (Files.exists(EXPLODED_DIR))
  73             FileUtils.deleteFileTreeWithRetry(EXPLODED_DIR);
  74 
  75         for (String name : new String[] { "foo"/*, "bar", "baz"*/ } ) {
  76             Path dir = EXPLODED_DIR.resolve(name);
  77             assertTrue(compileModule(name, dir.resolve("classes")));
  78             createCmds(dir.resolve("bin"));
  79             createLibs(dir.resolve("lib"));
  80             createConfigs(dir.resolve("conf"));
  81         }
  82 
  83         if (Files.exists(MODS_DIR))
  84             FileUtils.deleteFileTreeWithRetry(MODS_DIR);
  85         Files.createDirectories(MODS_DIR);
  86     }
  87 
  88     @Test
  89     public void testList() throws IOException {
  90         String cp = EXPLODED_DIR.resolve("foo").resolve("classes").toString();
  91         jmod("create",
  92              "--class-path", cp,
  93              MODS_DIR.resolve("foo.jmod").toString())
  94             .assertSuccess();
  95 
  96         jmod("list",
  97              MODS_DIR.resolve("foo.jmod").toString())
  98             .assertSuccess()
  99             .resultChecker(r -> {
 100                 // asserts dependent on the exact contents of foo
 101                 assertContains(r.output, CLASSES_PREFIX + "module-info.class");
 102                 assertContains(r.output, CLASSES_PREFIX + "jdk/test/foo/Foo.class");
 103                 assertContains(r.output, CLASSES_PREFIX + "jdk/test/foo/internal/Message.class");
 104             });
 105     }
 106 
 107     @Test
 108     public void testMainClass() throws IOException {
 109         Path jmod = MODS_DIR.resolve("fooMainClass.jmod");
 110         FileUtils.deleteFileIfExistsWithRetry(jmod);
 111         String cp = EXPLODED_DIR.resolve("foo").resolve("classes").toString();
 112 
 113         jmod("create",
 114              "--class-path", cp,
 115              "--main-class", "jdk.test.foo.Foo",
 116              jmod.toString())
 117             .assertSuccess()
 118             .resultChecker(r -> {
 119                 Optional<String> omc = getModuleDescriptor(jmod).mainClass();
 120                 assertTrue(omc.isPresent());
 121                 assertEquals(omc.get(), "jdk.test.foo.Foo");
 122             });
 123     }
 124 
 125     @Test
 126     public void testModuleVersion() throws IOException {
 127         Path jmod = MODS_DIR.resolve("fooVersion.jmod");
 128         FileUtils.deleteFileIfExistsWithRetry(jmod);
 129         String cp = EXPLODED_DIR.resolve("foo").resolve("classes").toString();
 130 
 131         jmod("create",
 132              "--class-path", cp,
 133              "--module-version", "5.4.3",
 134              jmod.toString())
 135             .assertSuccess()
 136             .resultChecker(r -> {
 137                 Optional<Version> ov = getModuleDescriptor(jmod).version();
 138                 assertTrue(ov.isPresent());
 139                 assertEquals(ov.get().toString(), "5.4.3");
 140             });
 141     }
 142 
 143     @Test
 144     public void testConfig() throws IOException {
 145         Path jmod = MODS_DIR.resolve("fooConfig.jmod");
 146         FileUtils.deleteFileIfExistsWithRetry(jmod);
 147         Path cp = EXPLODED_DIR.resolve("foo").resolve("classes");
 148         Path cf = EXPLODED_DIR.resolve("foo").resolve("conf");
 149 
 150         jmod("create",
 151              "--class-path", cp.toString(),
 152              "--config", cf.toString(),
 153              jmod.toString())
 154             .assertSuccess()
 155             .resultChecker(r -> {
 156                 try (Stream<String> s1 = findFiles(cf).map(p -> CONFIGS_PREFIX + p);
 157                      Stream<String> s2 = findFiles(cp).map(p -> CLASSES_PREFIX + p)) {
 158                     Set<String> expectedFilenames = Stream.concat(s1, s2)
 159                                                           .collect(toSet());
 160                     assertJmodContent(jmod, expectedFilenames);
 161                 }
 162             });
 163     }
 164 
 165     @Test
 166     public void testCmds() throws IOException {
 167         Path jmod = MODS_DIR.resolve("fooCmds.jmod");
 168         FileUtils.deleteFileIfExistsWithRetry(jmod);
 169         Path cp = EXPLODED_DIR.resolve("foo").resolve("classes");
 170         Path bp = EXPLODED_DIR.resolve("foo").resolve("bin");
 171 
 172         jmod("create",
 173              "--cmds", bp.toString(),
 174              "--class-path", cp.toString(),
 175              jmod.toString())
 176             .assertSuccess()
 177             .resultChecker(r -> {
 178                 try (Stream<String> s1 = findFiles(bp).map(p -> CMDS_PREFIX + p);
 179                      Stream<String> s2 = findFiles(cp).map(p -> CLASSES_PREFIX + p)) {
 180                     Set<String> expectedFilenames = Stream.concat(s1,s2)
 181                                                           .collect(toSet());
 182                     assertJmodContent(jmod, expectedFilenames);
 183                 }
 184             });
 185     }
 186 
 187     @Test
 188     public void testLibs() throws IOException {
 189         Path jmod = MODS_DIR.resolve("fooLibs.jmod");
 190         FileUtils.deleteFileIfExistsWithRetry(jmod);
 191         Path cp = EXPLODED_DIR.resolve("foo").resolve("classes");
 192         Path lp = EXPLODED_DIR.resolve("foo").resolve("lib");
 193 
 194         jmod("create",
 195              "--libs=", lp.toString(),
 196              "--class-path", cp.toString(),
 197              jmod.toString())
 198             .assertSuccess()
 199             .resultChecker(r -> {
 200                 try (Stream<String> s1 = findFiles(lp).map(p -> LIBS_PREFIX + p);
 201                      Stream<String> s2 = findFiles(cp).map(p -> CLASSES_PREFIX + p)) {
 202                     Set<String> expectedFilenames = Stream.concat(s1,s2)
 203                                                           .collect(toSet());
 204                     assertJmodContent(jmod, expectedFilenames);
 205                 }
 206             });
 207     }
 208 
 209     @Test
 210     public void testAll() throws IOException {
 211         Path jmod = MODS_DIR.resolve("fooAll.jmod");
 212         FileUtils.deleteFileIfExistsWithRetry(jmod);
 213         Path cp = EXPLODED_DIR.resolve("foo").resolve("classes");
 214         Path bp = EXPLODED_DIR.resolve("foo").resolve("bin");
 215         Path lp = EXPLODED_DIR.resolve("foo").resolve("lib");
 216         Path cf = EXPLODED_DIR.resolve("foo").resolve("conf");
 217 
 218         jmod("create",
 219              "--conf", cf.toString(),
 220              "--cmds=", bp.toString(),
 221              "--libs=", lp.toString(),
 222              "--class-path", cp.toString(),
 223              jmod.toString())
 224             .assertSuccess()
 225             .resultChecker(r -> {
 226                 try (Stream<String> s1 = findFiles(lp).map(p -> LIBS_PREFIX + p);
 227                      Stream<String> s2 = findFiles(cp).map(p -> CLASSES_PREFIX + p);
 228                      Stream<String> s3 = findFiles(bp).map(p -> CMDS_PREFIX + p);
 229                      Stream<String> s4 = findFiles(cf).map(p -> CONFIGS_PREFIX + p)) {
 230                     Set<String> expectedFilenames = Stream.concat(Stream.concat(s1,s2),
 231                                                                   Stream.concat(s3, s4))
 232                                                           .collect(toSet());
 233                     assertJmodContent(jmod, expectedFilenames);
 234                 }
 235             });
 236     }
 237 
 238     @Test
 239     public void testExcludes() throws IOException {
 240         Path jmod = MODS_DIR.resolve("fooLibs.jmod");
 241         FileUtils.deleteFileIfExistsWithRetry(jmod);
 242         Path cp = EXPLODED_DIR.resolve("foo").resolve("classes");
 243         Path lp = EXPLODED_DIR.resolve("foo").resolve("lib");
 244 
 245         jmod("create",
 246              "--libs=", lp.toString(),
 247              "--class-path", cp.toString(),
 248              "--exclude", "**internal**",
 249              "--exclude", "first.so",
 250              jmod.toString())
 251              .assertSuccess()
 252              .resultChecker(r -> {
 253                  Set<String> expectedFilenames = new HashSet<>();
 254                  expectedFilenames.add(CLASSES_PREFIX + "module-info.class");
 255                  expectedFilenames.add(CLASSES_PREFIX + "jdk/test/foo/Foo.class");
 256                  expectedFilenames.add(LIBS_PREFIX + "second.so");
 257                  expectedFilenames.add(LIBS_PREFIX + "third/third.so");
 258                  assertJmodContent(jmod, expectedFilenames);
 259 
 260                  Set<String> unexpectedFilenames = new HashSet<>();
 261                  unexpectedFilenames.add(CLASSES_PREFIX + "jdk/test/foo/internal/Message.class");
 262                  unexpectedFilenames.add(LIBS_PREFIX + "first.so");
 263                  assertJmodDoesNotContain(jmod, unexpectedFilenames);
 264              });
 265     }
 266 
 267     @Test
 268     public void describe() throws IOException {
 269         String cp = EXPLODED_DIR.resolve("foo").resolve("classes").toString();
 270         jmod("create",
 271              "--class-path", cp,
 272               MODS_DIR.resolve("describeFoo.jmod").toString())
 273              .assertSuccess();
 274 
 275         jmod("describe",
 276              MODS_DIR.resolve("describeFoo.jmod").toString())
 277              .assertSuccess()
 278              .resultChecker(r -> {
 279                  // Expect similar output: "foo,  requires mandated java.base
 280                  // exports jdk.test.foo,  conceals jdk.test.foo.internal"
 281                  Pattern p = Pattern.compile("\\s+foo\\s+requires\\s+mandated\\s+java.base");
 282                  assertTrue(p.matcher(r.output).find(),
 283                            "Expecting to find \"foo, requires java.base\"" +
 284                                 "in output, but did not: [" + r.output + "]");
 285                  p = Pattern.compile(
 286                         "exports\\s+jdk.test.foo\\s+conceals\\s+jdk.test.foo.internal");
 287                  assertTrue(p.matcher(r.output).find(),
 288                            "Expecting to find \"exports ..., conceals ...\"" +
 289                                 "in output, but did not: [" + r.output + "]");
 290              });
 291     }
 292 
 293     @Test
 294     public void testDuplicateEntries() throws IOException {
 295         Path jmod = MODS_DIR.resolve("testDuplicates.jmod");
 296         FileUtils.deleteFileIfExistsWithRetry(jmod);
 297         String cp = EXPLODED_DIR.resolve("foo").resolve("classes").toString();
 298         Path lp = EXPLODED_DIR.resolve("foo").resolve("lib");
 299 
 300         jmod("create",
 301              "--class-path", cp + pathSeparator + cp,
 302              jmod.toString())
 303              .assertSuccess()
 304              .resultChecker(r ->
 305                  assertContains(r.output, "Warning: ignoring duplicate entry")
 306              );
 307 
 308         FileUtils.deleteFileIfExistsWithRetry(jmod);
 309         jmod("create",
 310              "--class-path", cp,
 311              "--libs", lp.toString() + pathSeparator + lp.toString(),
 312              jmod.toString())
 313              .assertSuccess()
 314              .resultChecker(r ->
 315                  assertContains(r.output, "Warning: ignoring duplicate entry")
 316              );
 317     }
 318 
 319     @Test
 320     public void testIgnoreModuleInfoInOtherSections() throws IOException {
 321         Path jmod = MODS_DIR.resolve("testIgnoreModuleInfoInOtherSections.jmod");
 322         FileUtils.deleteFileIfExistsWithRetry(jmod);
 323         String cp = EXPLODED_DIR.resolve("foo").resolve("classes").toString();
 324 
 325         jmod("create",
 326             "--class-path", cp,
 327             "--libs", cp,
 328             jmod.toString())
 329             .assertSuccess()
 330             .resultChecker(r ->
 331                 assertContains(r.output, "Warning: ignoring entry")
 332             );
 333 
 334         FileUtils.deleteFileIfExistsWithRetry(jmod);
 335         jmod("create",
 336              "--class-path", cp,
 337              "--cmds", cp,
 338              jmod.toString())
 339              .assertSuccess()
 340              .resultChecker(r ->
 341                  assertContains(r.output, "Warning: ignoring entry")
 342              );
 343     }
 344 
 345     @Test
 346     public void testVersion() {
 347         jmod("--version")
 348             .assertSuccess()
 349             .resultChecker(r -> {
 350                 assertContains(r.output, System.getProperty("java.version"));
 351             });
 352     }
 353 
 354     @Test
 355     public void testHelp() {
 356         jmod("--help")
 357             .assertSuccess()
 358             .resultChecker(r ->
 359                 assertTrue(r.output.startsWith("Usage: jmod"), "Help not printed")
 360             );
 361     }
 362 
 363     @Test
 364     public void testTmpFileAlreadyExists() throws IOException {
 365         // Implementation detail: jmod tool creates <jmod-file>.tmp
 366         // Ensure that there are no problems if existing
 367 
 368         Path jmod = MODS_DIR.resolve("testTmpFileAlreadyExists.jmod");
 369         Path tmp = MODS_DIR.resolve("testTmpFileAlreadyExists.jmod.tmp");
 370         FileUtils.deleteFileIfExistsWithRetry(jmod);
 371         FileUtils.deleteFileIfExistsWithRetry(tmp);
 372         Files.createFile(tmp);
 373         String cp = EXPLODED_DIR.resolve("foo").resolve("classes").toString();
 374 
 375         jmod("create",
 376              "--class-path", cp,
 377              jmod.toString())
 378             .assertSuccess()
 379             .resultChecker(r ->
 380                 assertTrue(Files.notExists(tmp), "Unexpected tmp file:" + tmp)
 381             );
 382     }
 383 
 384     @Test
 385     public void testTmpFileRemoved() throws IOException {
 386         // Implementation detail: jmod tool creates <jmod-file>.tmp
 387         // Ensure that it is removed in the event of a failure.
 388         // The failure in this case is a class in the unnamed package.
 389 
 390         Path jmod = MODS_DIR.resolve("testTmpFileRemoved.jmod");
 391         Path tmp = MODS_DIR.resolve("testTmpFileRemoved.jmod.tmp");
 392         FileUtils.deleteFileIfExistsWithRetry(jmod);
 393         FileUtils.deleteFileIfExistsWithRetry(tmp);
 394         String cp = EXPLODED_DIR.resolve("foo").resolve("classes") + File.pathSeparator +
 395                     EXPLODED_DIR.resolve("foo").resolve("classes")
 396                                 .resolve("jdk").resolve("test").resolve("foo").toString();
 397 
 398         jmod("create",
 399              "--class-path", cp,
 400              jmod.toString())
 401              .assertFailure()
 402              .resultChecker(r -> {
 403                  assertContains(r.output, "unnamed package");
 404                  assertTrue(Files.notExists(tmp), "Unexpected tmp file:" + tmp);
 405              });
 406     }
 407 
 408     // ---
 409 
 410     static boolean compileModule(String name, Path dest) throws IOException {
 411         return CompilerUtils.compile(SRC_DIR.resolve(name), dest);
 412     }
 413 
 414     static void assertContains(String output, String subString) {
 415         if (output.contains(subString))
 416             assertTrue(true);
 417         else
 418             assertTrue(false,"Expected to find [" + subString + "], in output ["
 419                            + output + "]" + "\n");
 420     }
 421 
 422     static ModuleDescriptor getModuleDescriptor(Path jmod) {
 423         ClassLoader cl = ClassLoader.getSystemClassLoader();
 424         try (FileSystem fs = FileSystems.newFileSystem(jmod, cl)) {
 425             String p = "/classes/module-info.class";
 426             try (InputStream is = Files.newInputStream(fs.getPath(p))) {
 427                 return ModuleDescriptor.read(is);
 428             }
 429         } catch (IOException ioe) {
 430             throw new UncheckedIOException(ioe);
 431         }
 432     }
 433 
 434     static Stream<String> findFiles(Path dir) {
 435         try {
 436             return Files.find(dir, Integer.MAX_VALUE, (p, a) -> a.isRegularFile())
 437                         .map(dir::relativize)
 438                         .map(Path::toString)
 439                         .map(p -> p.replace(File.separator, "/"));
 440         } catch (IOException x) {
 441             throw new UncheckedIOException(x);
 442         }
 443     }
 444 
 445     static Set<String> getJmodContent(Path jmod) {
 446         JmodResult r = jmod("list", jmod.toString()).assertSuccess();
 447         return Stream.of(r.output.split("\r?\n")).collect(toSet());
 448     }
 449 
 450     static void assertJmodContent(Path jmod, Set<String> expected) {
 451         Set<String> actual = getJmodContent(jmod);
 452         if (!Objects.equals(actual, expected)) {
 453             Set<String> unexpected = new HashSet<>(actual);
 454             unexpected.removeAll(expected);
 455             Set<String> notFound = new HashSet<>(expected);
 456             notFound.removeAll(actual);
 457             StringBuilder sb = new StringBuilder();
 458             sb.append("Unexpected but found:\n");
 459             unexpected.forEach(s -> sb.append("\t" + s + "\n"));
 460             sb.append("Expected but not found:\n");
 461             notFound.forEach(s -> sb.append("\t" + s + "\n"));
 462             assertTrue(false, "Jmod content check failed.\n" + sb.toString());
 463         }
 464     }
 465 
 466     static void assertJmodDoesNotContain(Path jmod, Set<String> unexpectedNames) {
 467         Set<String> actual = getJmodContent(jmod);
 468         Set<String> unexpected = new HashSet<>();
 469         for (String name : unexpectedNames) {
 470             if (actual.contains(name))
 471                 unexpected.add(name);
 472         }
 473         if (!unexpected.isEmpty()) {
 474             StringBuilder sb = new StringBuilder();
 475             for (String s : unexpected)
 476                 sb.append("Unexpected but found: " + s + "\n");
 477             sb.append("In :");
 478             for (String s : actual)
 479                 sb.append("\t" + s + "\n");
 480             assertTrue(false, "Jmod content check failed.\n" + sb.toString());
 481         }
 482     }
 483 
 484     static JmodResult jmod(String... args) {
 485         ByteArrayOutputStream baos = new ByteArrayOutputStream();
 486         PrintStream ps = new PrintStream(baos);
 487         System.out.println("jmod " + Arrays.asList(args));
 488         int ec = JMOD_TOOL.run(ps, ps, args);
 489         return new JmodResult(ec, new String(baos.toByteArray(), UTF_8));
 490     }
 491 
 492     static class JmodResult {
 493         final int exitCode;
 494         final String output;
 495 
 496         JmodResult(int exitValue, String output) {
 497             this.exitCode = exitValue;
 498             this.output = output;
 499         }
 500         JmodResult assertSuccess() { assertTrue(exitCode == 0, output); return this; }
 501         JmodResult assertFailure() { assertTrue(exitCode != 0, output); return this; }
 502         JmodResult resultChecker(Consumer<JmodResult> r) { r.accept(this); return this; }
 503     }
 504 
 505     static void createCmds(Path dir) throws IOException {
 506         List<String> files = Arrays.asList(
 507                 "first", "second", "third" + File.separator + "third");
 508         createFiles(dir, files);
 509     }
 510 
 511     static void createLibs(Path dir) throws IOException {
 512         List<String> files = Arrays.asList(
 513                 "first.so", "second.so", "third" + File.separator + "third.so");
 514         createFiles(dir, files);
 515     }
 516 
 517     static void createConfigs(Path dir) throws IOException {
 518         List<String> files = Arrays.asList(
 519                 "first.cfg", "second.cfg", "third" + File.separator + "third.cfg");
 520         createFiles(dir, files);
 521     }
 522 
 523     static void createFiles(Path dir, List<String> filenames) throws IOException {
 524         for (String name : filenames) {
 525             Path file = dir.resolve(name);
 526             Files.createDirectories(file.getParent());
 527             Files.createFile(file);
 528             try (OutputStream os  = Files.newOutputStream(file)) {
 529                 os.write("blahblahblah".getBytes(UTF_8));
 530             }
 531         }
 532     }
 533 
 534     // Standalone entry point.
 535     public static void main(String[] args) throws Throwable {
 536         JmodTest test = new JmodTest();
 537         test.buildExplodedModules();
 538         for (Method m : JmodTest.class.getDeclaredMethods()) {
 539             if (m.getAnnotation(Test.class) != null) {
 540                 System.out.println("Invoking " + m.getName());
 541                 m.invoke(test);
 542             }
 543         }
 544     }
 545 }