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