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  * @bug 8142968 8166568
  27  * @summary Basic test for jmod
  28  * @library /lib/testlibrary
  29  * @modules jdk.compiler
  30  *          jdk.jlink
  31  * @build jdk.testlibrary.FileUtils CompilerUtils
  32  * @run testng JmodTest
  33  */
  34 
  35 import java.io.*;
  36 import java.lang.module.ModuleDescriptor;
  37 import java.lang.reflect.Method;
  38 import java.nio.file.*;
  39 import java.util.*;
  40 import java.util.function.Consumer;
  41 import java.util.regex.Pattern;
  42 import java.util.spi.ToolProvider;
  43 import java.util.stream.Stream;
  44 import jdk.testlibrary.FileUtils;
  45 import org.testng.annotations.BeforeTest;
  46 import org.testng.annotations.Test;
  47 
  48 import static java.io.File.pathSeparator;
  49 import static java.lang.module.ModuleDescriptor.Version;
  50 import static java.nio.charset.StandardCharsets.UTF_8;
  51 import static java.util.stream.Collectors.toSet;
  52 import static org.testng.Assert.*;
  53 
  54 public class JmodTest {
  55 
  56     static final ToolProvider JMOD_TOOL = ToolProvider.findFirst("jmod")
  57         .orElseThrow(() ->
  58             new RuntimeException("jmod tool not found")
  59         );
  60 
  61     static final String TEST_SRC = System.getProperty("test.src", ".");
  62     static final Path SRC_DIR = Paths.get(TEST_SRC, "src");
  63     static final Path EXPLODED_DIR = Paths.get("build");
  64     static final Path MODS_DIR = Paths.get("jmods");
  65 
  66     static final String CLASSES_PREFIX = "classes/";
  67     static final String CMDS_PREFIX = "bin/";
  68     static final String LIBS_PREFIX = "native/";
  69     static final String CONFIGS_PREFIX = "conf/";
  70 
  71     @BeforeTest
  72     public void buildExplodedModules() throws IOException {
  73         if (Files.exists(EXPLODED_DIR))
  74             FileUtils.deleteFileTreeWithRetry(EXPLODED_DIR);
  75 
  76         for (String name : new String[] { "foo"/*, "bar", "baz"*/ } ) {
  77             Path dir = EXPLODED_DIR.resolve(name);
  78             assertTrue(compileModule(name, dir.resolve("classes")));
  79             copyResource(SRC_DIR.resolve("foo"),
  80                          dir.resolve("classes"),
  81                          "jdk/test/foo/resources/foo.properties");
  82             createCmds(dir.resolve("bin"));
  83             createLibs(dir.resolve("lib"));
  84             createConfigs(dir.resolve("conf"));
  85         }
  86 
  87         if (Files.exists(MODS_DIR))
  88             FileUtils.deleteFileTreeWithRetry(MODS_DIR);
  89         Files.createDirectories(MODS_DIR);
  90     }
  91 
  92     // JDK-8166286 - jmod fails on symlink to directory
  93     @Test
  94     public void testSymlinks() throws IOException {
  95         Path apaDir = EXPLODED_DIR.resolve("apa");
  96         Path classesDir = EXPLODED_DIR.resolve("apa").resolve("classes");
  97         assertTrue(compileModule("apa", classesDir));
  98         Path libDir = apaDir.resolve("lib");
  99         createFiles(libDir, List.of("foo/bar/libfoo.so"));
 100         try {
 101             Path link = Files.createSymbolicLink(
 102                 libDir.resolve("baz"), libDir.resolve("foo").toAbsolutePath());
 103             assertTrue(Files.exists(link));
 104         } catch (UnsupportedOperationException uoe) {
 105             // OS does not support symlinks. Nothing to test!
 106             return;
 107         }
 108 
 109         Path jmod = MODS_DIR.resolve("apa.jmod");
 110         jmod("create",
 111              "--libs=", libDir.toString(),
 112              "--class-path", classesDir.toString(),
 113              jmod.toString())
 114             .assertSuccess();
 115     }
 116 
 117     @Test
 118     public void testList() throws IOException {
 119         String cp = EXPLODED_DIR.resolve("foo").resolve("classes").toString();
 120         jmod("create",
 121              "--class-path", cp,
 122              MODS_DIR.resolve("foo.jmod").toString())
 123             .assertSuccess();
 124 
 125         jmod("list",
 126              MODS_DIR.resolve("foo.jmod").toString())
 127             .assertSuccess()
 128             .resultChecker(r -> {
 129                 // asserts dependent on the exact contents of foo
 130                 assertContains(r.output, CLASSES_PREFIX + "module-info.class");
 131                 assertContains(r.output, CLASSES_PREFIX + "jdk/test/foo/Foo.class");
 132                 assertContains(r.output, CLASSES_PREFIX + "jdk/test/foo/internal/Message.class");
 133                 assertContains(r.output, CLASSES_PREFIX + "jdk/test/foo/resources/foo.properties");
 134             });
 135     }
 136 
 137     @Test
 138     public void testExtractCWD() throws IOException {
 139         Path cp = EXPLODED_DIR.resolve("foo").resolve("classes");
 140         jmod("create",
 141              "--class-path", cp.toString(),
 142              MODS_DIR.resolve("fooExtractCWD.jmod").toString())
 143             .assertSuccess();
 144 
 145         jmod("extract",
 146              MODS_DIR.resolve("fooExtractCWD.jmod").toString())
 147             .assertSuccess()
 148             .resultChecker(r -> {
 149                 // module-info should exist, but jmod will have added its Packages attr.
 150                 assertTrue(Files.exists(Paths.get("classes/module-info.class")));
 151                 assertSameContent(cp.resolve("jdk/test/foo/Foo.class"),
 152                                   Paths.get("classes/jdk/test/foo/Foo.class"));
 153                 assertSameContent(cp.resolve("jdk/test/foo/internal/Message.class"),
 154                                   Paths.get("classes/jdk/test/foo/internal/Message.class"));
 155                 assertSameContent(cp.resolve("jdk/test/foo/resources/foo.properties"),
 156                                   Paths.get("classes/jdk/test/foo/resources/foo.properties"));
 157             });
 158     }
 159 
 160     @Test
 161     public void testExtractDir() throws IOException {
 162         if (Files.exists(Paths.get("extractTestDir")))
 163             FileUtils.deleteFileTreeWithRetry(Paths.get("extractTestDir"));
 164         Path cp = EXPLODED_DIR.resolve("foo").resolve("classes");
 165         Path bp = EXPLODED_DIR.resolve("foo").resolve("bin");
 166         Path lp = EXPLODED_DIR.resolve("foo").resolve("lib");
 167         Path cf = EXPLODED_DIR.resolve("foo").resolve("conf");
 168 
 169         jmod("create",
 170              "--conf", cf.toString(),
 171              "--cmds", bp.toString(),
 172              "--libs", lp.toString(),
 173              "--class-path", cp.toString(),
 174              MODS_DIR.resolve("fooExtractDir.jmod").toString())
 175             .assertSuccess();
 176 
 177         jmod("extract",
 178              "--dir", "extractTestDir",
 179              MODS_DIR.resolve("fooExtractDir.jmod").toString())
 180             .assertSuccess();
 181 
 182         jmod("extract",
 183              "--dir", "extractTestDir",
 184              MODS_DIR.resolve("fooExtractDir.jmod").toString())
 185             .assertSuccess()
 186             .resultChecker(r -> {
 187                 // check a sample of the extracted files
 188                 Path p = Paths.get("extractTestDir");
 189                 assertTrue(Files.exists(p.resolve("classes/module-info.class")));
 190                 assertSameContent(cp.resolve("jdk/test/foo/Foo.class"),
 191                                   p.resolve("classes/jdk/test/foo/Foo.class"));
 192                 assertSameContent(bp.resolve("first"),
 193                                   p.resolve(CMDS_PREFIX).resolve("first"));
 194                 assertSameContent(lp.resolve("first.so"),
 195                                   p.resolve(LIBS_PREFIX).resolve("second.so"));
 196                 assertSameContent(cf.resolve("second.cfg"),
 197                                   p.resolve(CONFIGS_PREFIX).resolve("second.cfg"));
 198             });
 199     }
 200 
 201     @Test
 202     public void testMainClass() throws IOException {
 203         Path jmod = MODS_DIR.resolve("fooMainClass.jmod");
 204         FileUtils.deleteFileIfExistsWithRetry(jmod);
 205         String cp = EXPLODED_DIR.resolve("foo").resolve("classes").toString();
 206 
 207         jmod("create",
 208              "--class-path", cp,
 209              "--main-class", "jdk.test.foo.Foo",
 210              jmod.toString())
 211             .assertSuccess()
 212             .resultChecker(r -> {
 213                 Optional<String> omc = getModuleDescriptor(jmod).mainClass();
 214                 assertTrue(omc.isPresent());
 215                 assertEquals(omc.get(), "jdk.test.foo.Foo");
 216             });
 217     }
 218 
 219     @Test
 220     public void testModuleVersion() throws IOException {
 221         Path jmod = MODS_DIR.resolve("fooVersion.jmod");
 222         FileUtils.deleteFileIfExistsWithRetry(jmod);
 223         String cp = EXPLODED_DIR.resolve("foo").resolve("classes").toString();
 224 
 225         jmod("create",
 226              "--class-path", cp,
 227              "--module-version", "5.4.3",
 228              jmod.toString())
 229             .assertSuccess()
 230             .resultChecker(r -> {
 231                 Optional<Version> ov = getModuleDescriptor(jmod).version();
 232                 assertTrue(ov.isPresent());
 233                 assertEquals(ov.get().toString(), "5.4.3");
 234             });
 235     }
 236 
 237     @Test
 238     public void testConfig() throws IOException {
 239         Path jmod = MODS_DIR.resolve("fooConfig.jmod");
 240         FileUtils.deleteFileIfExistsWithRetry(jmod);
 241         Path cp = EXPLODED_DIR.resolve("foo").resolve("classes");
 242         Path cf = EXPLODED_DIR.resolve("foo").resolve("conf");
 243 
 244         jmod("create",
 245              "--class-path", cp.toString(),
 246              "--config", cf.toString(),
 247              jmod.toString())
 248             .assertSuccess()
 249             .resultChecker(r -> {
 250                 try (Stream<String> s1 = findFiles(cf).map(p -> CONFIGS_PREFIX + p);
 251                      Stream<String> s2 = findFiles(cp).map(p -> CLASSES_PREFIX + p)) {
 252                     Set<String> expectedFilenames = Stream.concat(s1, s2)
 253                                                           .collect(toSet());
 254                     assertJmodContent(jmod, expectedFilenames);
 255                 }
 256             });
 257     }
 258 
 259     @Test
 260     public void testCmds() throws IOException {
 261         Path jmod = MODS_DIR.resolve("fooCmds.jmod");
 262         FileUtils.deleteFileIfExistsWithRetry(jmod);
 263         Path cp = EXPLODED_DIR.resolve("foo").resolve("classes");
 264         Path bp = EXPLODED_DIR.resolve("foo").resolve("bin");
 265 
 266         jmod("create",
 267              "--cmds", bp.toString(),
 268              "--class-path", cp.toString(),
 269              jmod.toString())
 270             .assertSuccess()
 271             .resultChecker(r -> {
 272                 try (Stream<String> s1 = findFiles(bp).map(p -> CMDS_PREFIX + p);
 273                      Stream<String> s2 = findFiles(cp).map(p -> CLASSES_PREFIX + p)) {
 274                     Set<String> expectedFilenames = Stream.concat(s1,s2)
 275                                                           .collect(toSet());
 276                     assertJmodContent(jmod, expectedFilenames);
 277                 }
 278             });
 279     }
 280 
 281     @Test
 282     public void testLibs() throws IOException {
 283         Path jmod = MODS_DIR.resolve("fooLibs.jmod");
 284         FileUtils.deleteFileIfExistsWithRetry(jmod);
 285         Path cp = EXPLODED_DIR.resolve("foo").resolve("classes");
 286         Path lp = EXPLODED_DIR.resolve("foo").resolve("lib");
 287 
 288         jmod("create",
 289              "--libs=", lp.toString(),
 290              "--class-path", cp.toString(),
 291              jmod.toString())
 292             .assertSuccess()
 293             .resultChecker(r -> {
 294                 try (Stream<String> s1 = findFiles(lp).map(p -> LIBS_PREFIX + p);
 295                      Stream<String> s2 = findFiles(cp).map(p -> CLASSES_PREFIX + p)) {
 296                     Set<String> expectedFilenames = Stream.concat(s1,s2)
 297                                                           .collect(toSet());
 298                     assertJmodContent(jmod, expectedFilenames);
 299                 }
 300             });
 301     }
 302 
 303     @Test
 304     public void testAll() throws IOException {
 305         Path jmod = MODS_DIR.resolve("fooAll.jmod");
 306         FileUtils.deleteFileIfExistsWithRetry(jmod);
 307         Path cp = EXPLODED_DIR.resolve("foo").resolve("classes");
 308         Path bp = EXPLODED_DIR.resolve("foo").resolve("bin");
 309         Path lp = EXPLODED_DIR.resolve("foo").resolve("lib");
 310         Path cf = EXPLODED_DIR.resolve("foo").resolve("conf");
 311 
 312         jmod("create",
 313              "--conf", cf.toString(),
 314              "--cmds=", bp.toString(),
 315              "--libs=", lp.toString(),
 316              "--class-path", cp.toString(),
 317              jmod.toString())
 318             .assertSuccess()
 319             .resultChecker(r -> {
 320                 try (Stream<String> s1 = findFiles(lp).map(p -> LIBS_PREFIX + p);
 321                      Stream<String> s2 = findFiles(cp).map(p -> CLASSES_PREFIX + p);
 322                      Stream<String> s3 = findFiles(bp).map(p -> CMDS_PREFIX + p);
 323                      Stream<String> s4 = findFiles(cf).map(p -> CONFIGS_PREFIX + p)) {
 324                     Set<String> expectedFilenames = Stream.concat(Stream.concat(s1,s2),
 325                                                                   Stream.concat(s3, s4))
 326                                                           .collect(toSet());
 327                     assertJmodContent(jmod, expectedFilenames);
 328                 }
 329             });
 330     }
 331 
 332     @Test
 333     public void testExcludes() throws IOException {
 334         Path jmod = MODS_DIR.resolve("fooLibs.jmod");
 335         FileUtils.deleteFileIfExistsWithRetry(jmod);
 336         Path cp = EXPLODED_DIR.resolve("foo").resolve("classes");
 337         Path lp = EXPLODED_DIR.resolve("foo").resolve("lib");
 338 
 339         jmod("create",
 340              "--libs=", lp.toString(),
 341              "--class-path", cp.toString(),
 342              "--exclude", "**internal**",
 343              "--exclude", "first.so",
 344              jmod.toString())
 345              .assertSuccess()
 346              .resultChecker(r -> {
 347                  Set<String> expectedFilenames = new HashSet<>();
 348                  expectedFilenames.add(CLASSES_PREFIX + "module-info.class");
 349                  expectedFilenames.add(CLASSES_PREFIX + "jdk/test/foo/Foo.class");
 350                  expectedFilenames.add(CLASSES_PREFIX + "jdk/test/foo/resources/foo.properties");
 351                  expectedFilenames.add(LIBS_PREFIX + "second.so");
 352                  expectedFilenames.add(LIBS_PREFIX + "third/third.so");
 353                  assertJmodContent(jmod, expectedFilenames);
 354 
 355                  Set<String> unexpectedFilenames = new HashSet<>();
 356                  unexpectedFilenames.add(CLASSES_PREFIX + "jdk/test/foo/internal/Message.class");
 357                  unexpectedFilenames.add(LIBS_PREFIX + "first.so");
 358                  assertJmodDoesNotContain(jmod, unexpectedFilenames);
 359              });
 360     }
 361 
 362     @Test
 363     public void describe() throws IOException {
 364         String cp = EXPLODED_DIR.resolve("foo").resolve("classes").toString();
 365         jmod("create",
 366              "--class-path", cp,
 367               MODS_DIR.resolve("describeFoo.jmod").toString())
 368              .assertSuccess();
 369 
 370         jmod("describe",
 371              MODS_DIR.resolve("describeFoo.jmod").toString())
 372              .assertSuccess()
 373              .resultChecker(r -> {
 374                  // Expect similar output: "foo,  requires mandated java.base
 375                  // exports jdk.test.foo,  contains jdk.test.foo.internal"
 376                  Pattern p = Pattern.compile("\\s+foo\\s+requires\\s+mandated\\s+java.base");
 377                  assertTrue(p.matcher(r.output).find(),
 378                            "Expecting to find \"foo, requires java.base\"" +
 379                                 "in output, but did not: [" + r.output + "]");
 380                  p = Pattern.compile(
 381                         "exports\\s+jdk.test.foo\\s+contains\\s+jdk.test.foo.internal");
 382                  assertTrue(p.matcher(r.output).find(),
 383                            "Expecting to find \"exports ..., contains ...\"" +
 384                                 "in output, but did not: [" + r.output + "]");
 385              });
 386     }
 387 
 388     @Test
 389     public void testDuplicateEntries() throws IOException {
 390         Path jmod = MODS_DIR.resolve("testDuplicates.jmod");
 391         FileUtils.deleteFileIfExistsWithRetry(jmod);
 392         String cp = EXPLODED_DIR.resolve("foo").resolve("classes").toString();
 393         Path lp = EXPLODED_DIR.resolve("foo").resolve("lib");
 394 
 395         jmod("create",
 396              "--class-path", cp + pathSeparator + cp,
 397              jmod.toString())
 398              .assertSuccess()
 399              .resultChecker(r ->
 400                  assertContains(r.output, "Warning: ignoring duplicate entry")
 401              );
 402 
 403         FileUtils.deleteFileIfExistsWithRetry(jmod);
 404         jmod("create",
 405              "--class-path", cp,
 406              "--libs", lp.toString() + pathSeparator + lp.toString(),
 407              jmod.toString())
 408              .assertSuccess()
 409              .resultChecker(r ->
 410                  assertContains(r.output, "Warning: ignoring duplicate entry")
 411              );
 412     }
 413 
 414     @Test
 415     public void testIgnoreModuleInfoInOtherSections() throws IOException {
 416         Path jmod = MODS_DIR.resolve("testIgnoreModuleInfoInOtherSections.jmod");
 417         FileUtils.deleteFileIfExistsWithRetry(jmod);
 418         String cp = EXPLODED_DIR.resolve("foo").resolve("classes").toString();
 419 
 420         jmod("create",
 421             "--class-path", cp,
 422             "--libs", cp,
 423             jmod.toString())
 424             .assertSuccess()
 425             .resultChecker(r ->
 426                 assertContains(r.output, "Warning: ignoring entry")
 427             );
 428 
 429         FileUtils.deleteFileIfExistsWithRetry(jmod);
 430         jmod("create",
 431              "--class-path", cp,
 432              "--cmds", cp,
 433              jmod.toString())
 434              .assertSuccess()
 435              .resultChecker(r ->
 436                  assertContains(r.output, "Warning: ignoring entry")
 437              );
 438     }
 439 
 440     @Test
 441     public void testPackagesAttribute() throws IOException {
 442         Path jmod = MODS_DIR.resolve("foo.jmod");
 443         FileUtils.deleteFileIfExistsWithRetry(jmod);
 444         String cp = EXPLODED_DIR.resolve("foo").resolve("classes").toString();
 445 
 446         Set<String> expectedPackages = Set.of("jdk.test.foo",
 447                                               "jdk.test.foo.internal",
 448                                               "jdk.test.foo.resources");
 449 
 450         jmod("create",
 451              "--class-path", cp,
 452              jmod.toString())
 453              .assertSuccess()
 454              .resultChecker(r -> {
 455                  Set<String> pkgs = getModuleDescriptor(jmod).packages();
 456                  assertEquals(pkgs, expectedPackages);
 457              });
 458         }
 459 
 460     @Test
 461     public void testVersion() {
 462         jmod("--version")
 463             .assertSuccess()
 464             .resultChecker(r -> {
 465                 assertContains(r.output, System.getProperty("java.version"));
 466             });
 467     }
 468 
 469     @Test
 470     public void testHelp() {
 471         jmod("--help")
 472             .assertSuccess()
 473             .resultChecker(r -> {
 474                 assertTrue(r.output.startsWith("Usage: jmod"), "Help not printed");
 475                 assertFalse(r.output.contains("--do-not-resolve-by-default"));
 476                 assertFalse(r.output.contains("--warn-if-resolved"));
 477             });
 478     }
 479 
 480     @Test
 481     public void testHelpExtra() {
 482         jmod("--help-extra")
 483             .assertSuccess()
 484             .resultChecker(r -> {
 485                 assertTrue(r.output.startsWith("Usage: jmod"), "Extra help not printed");
 486                 assertContains(r.output, "--do-not-resolve-by-default");
 487                 assertContains(r.output, "--warn-if-resolved");
 488             });
 489     }
 490 
 491     @Test
 492     public void testTmpFileAlreadyExists() throws IOException {
 493         // Implementation detail: jmod tool creates <jmod-file>.tmp
 494         // Ensure that there are no problems if existing
 495 
 496         Path jmod = MODS_DIR.resolve("testTmpFileAlreadyExists.jmod");
 497         Path tmp = MODS_DIR.resolve("testTmpFileAlreadyExists.jmod.tmp");
 498         FileUtils.deleteFileIfExistsWithRetry(jmod);
 499         FileUtils.deleteFileIfExistsWithRetry(tmp);
 500         Files.createFile(tmp);
 501         String cp = EXPLODED_DIR.resolve("foo").resolve("classes").toString();
 502 
 503         jmod("create",
 504              "--class-path", cp,
 505              jmod.toString())
 506             .assertSuccess()
 507             .resultChecker(r ->
 508                 assertTrue(Files.notExists(tmp), "Unexpected tmp file:" + tmp)
 509             );
 510     }
 511 
 512     @Test
 513     public void testTmpFileRemoved() throws IOException {
 514         // Implementation detail: jmod tool creates <jmod-file>.tmp
 515         // Ensure that it is removed in the event of a failure.
 516         // The failure in this case is a class in the unnamed package.
 517 
 518         Path jmod = MODS_DIR.resolve("testTmpFileRemoved.jmod");
 519         Path tmp = MODS_DIR.resolve("testTmpFileRemoved.jmod.tmp");
 520         FileUtils.deleteFileIfExistsWithRetry(jmod);
 521         FileUtils.deleteFileIfExistsWithRetry(tmp);
 522         String cp = EXPLODED_DIR.resolve("foo").resolve("classes") + File.pathSeparator +
 523                     EXPLODED_DIR.resolve("foo").resolve("classes")
 524                                 .resolve("jdk").resolve("test").resolve("foo").toString();
 525 
 526         jmod("create",
 527              "--class-path", cp,
 528              jmod.toString())
 529              .assertFailure()
 530              .resultChecker(r -> {
 531                  assertContains(r.output, "unnamed package");
 532                  assertTrue(Files.notExists(tmp), "Unexpected tmp file:" + tmp);
 533              });
 534     }
 535 
 536     // ---
 537 
 538     static boolean compileModule(String name, Path dest) throws IOException {
 539         return CompilerUtils.compile(SRC_DIR.resolve(name), dest);
 540     }
 541 
 542     static void assertContains(String output, String subString) {
 543         if (output.contains(subString))
 544             assertTrue(true);
 545         else
 546             assertTrue(false,"Expected to find [" + subString + "], in output ["
 547                            + output + "]" + "\n");
 548     }
 549 
 550     static ModuleDescriptor getModuleDescriptor(Path jmod) {
 551         ClassLoader cl = ClassLoader.getSystemClassLoader();
 552         try (FileSystem fs = FileSystems.newFileSystem(jmod, cl)) {
 553             String p = "/classes/module-info.class";
 554             try (InputStream is = Files.newInputStream(fs.getPath(p))) {
 555                 return ModuleDescriptor.read(is);
 556             }
 557         } catch (IOException ioe) {
 558             throw new UncheckedIOException(ioe);
 559         }
 560     }
 561 
 562     static Stream<String> findFiles(Path dir) {
 563         try {
 564             return Files.find(dir, Integer.MAX_VALUE, (p, a) -> a.isRegularFile())
 565                         .map(dir::relativize)
 566                         .map(Path::toString)
 567                         .map(p -> p.replace(File.separator, "/"));
 568         } catch (IOException x) {
 569             throw new UncheckedIOException(x);
 570         }
 571     }
 572 
 573     static Set<String> getJmodContent(Path jmod) {
 574         JmodResult r = jmod("list", jmod.toString()).assertSuccess();
 575         return Stream.of(r.output.split("\r?\n")).collect(toSet());
 576     }
 577 
 578     static void assertJmodContent(Path jmod, Set<String> expected) {
 579         Set<String> actual = getJmodContent(jmod);
 580         if (!Objects.equals(actual, expected)) {
 581             Set<String> unexpected = new HashSet<>(actual);
 582             unexpected.removeAll(expected);
 583             Set<String> notFound = new HashSet<>(expected);
 584             notFound.removeAll(actual);
 585             StringBuilder sb = new StringBuilder();
 586             sb.append("Unexpected but found:\n");
 587             unexpected.forEach(s -> sb.append("\t" + s + "\n"));
 588             sb.append("Expected but not found:\n");
 589             notFound.forEach(s -> sb.append("\t" + s + "\n"));
 590             assertTrue(false, "Jmod content check failed.\n" + sb.toString());
 591         }
 592     }
 593 
 594     static void assertJmodDoesNotContain(Path jmod, Set<String> unexpectedNames) {
 595         Set<String> actual = getJmodContent(jmod);
 596         Set<String> unexpected = new HashSet<>();
 597         for (String name : unexpectedNames) {
 598             if (actual.contains(name))
 599                 unexpected.add(name);
 600         }
 601         if (!unexpected.isEmpty()) {
 602             StringBuilder sb = new StringBuilder();
 603             for (String s : unexpected)
 604                 sb.append("Unexpected but found: " + s + "\n");
 605             sb.append("In :");
 606             for (String s : actual)
 607                 sb.append("\t" + s + "\n");
 608             assertTrue(false, "Jmod content check failed.\n" + sb.toString());
 609         }
 610     }
 611 
 612     static void assertSameContent(Path p1, Path p2) {
 613         try {
 614             byte[] ba1 = Files.readAllBytes(p1);
 615             byte[] ba2 = Files.readAllBytes(p2);
 616             assertEquals(ba1, ba2);
 617         } catch (IOException x) {
 618             throw new UncheckedIOException(x);
 619         }
 620     }
 621 
 622     static JmodResult jmod(String... args) {
 623         ByteArrayOutputStream baos = new ByteArrayOutputStream();
 624         PrintStream ps = new PrintStream(baos);
 625         System.out.println("jmod " + Arrays.asList(args));
 626         int ec = JMOD_TOOL.run(ps, ps, args);
 627         return new JmodResult(ec, new String(baos.toByteArray(), UTF_8));
 628     }
 629 
 630     static class JmodResult {
 631         final int exitCode;
 632         final String output;
 633 
 634         JmodResult(int exitValue, String output) {
 635             this.exitCode = exitValue;
 636             this.output = output;
 637         }
 638         JmodResult assertSuccess() { assertTrue(exitCode == 0, output); return this; }
 639         JmodResult assertFailure() { assertTrue(exitCode != 0, output); return this; }
 640         JmodResult resultChecker(Consumer<JmodResult> r) { r.accept(this); return this; }
 641     }
 642 
 643     static void createCmds(Path dir) throws IOException {
 644         List<String> files = Arrays.asList(
 645                 "first", "second", "third" + File.separator + "third");
 646         createFiles(dir, files);
 647     }
 648 
 649     static void createLibs(Path dir) throws IOException {
 650         List<String> files = Arrays.asList(
 651                 "first.so", "second.so", "third" + File.separator + "third.so");
 652         createFiles(dir, files);
 653     }
 654 
 655     static void createConfigs(Path dir) throws IOException {
 656         List<String> files = Arrays.asList(
 657                 "first.cfg", "second.cfg", "third" + File.separator + "third.cfg");
 658         createFiles(dir, files);
 659     }
 660 
 661     static void createFiles(Path dir, List<String> filenames) throws IOException {
 662         for (String name : filenames) {
 663             Path file = dir.resolve(name);
 664             Files.createDirectories(file.getParent());
 665             Files.createFile(file);
 666             try (OutputStream os  = Files.newOutputStream(file)) {
 667                 os.write("blahblahblah".getBytes(UTF_8));
 668             }
 669         }
 670     }
 671 
 672     static void copyResource(Path srcDir, Path dir, String resource) throws IOException {
 673         Path dest = dir.resolve(resource);
 674         Files.deleteIfExists(dest);
 675 
 676         Files.createDirectories(dest.getParent());
 677         Files.copy(srcDir.resolve(resource), dest);
 678     }
 679 
 680     // Standalone entry point.
 681     public static void main(String[] args) throws Throwable {
 682         JmodTest test = new JmodTest();
 683         test.buildExplodedModules();
 684         for (Method m : JmodTest.class.getDeclaredMethods()) {
 685             if (m.getAnnotation(Test.class) != null) {
 686                 System.out.println("Invoking " + m.getName());
 687                 m.invoke(test);
 688             }
 689         }
 690     }
 691 }