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