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