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