1 /*
   2  * Copyright (c) 2016, 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 8146486 8172432
  27  * @summary Fail to create a MR modular JAR with a versioned entry in
  28  *          base-versioned empty package
  29  * @modules java.base/jdk.internal.module
  30  *          jdk.compiler
  31  *          jdk.jartool
  32  * @library /lib/testlibrary
  33  * @build jdk.testlibrary.FileUtils
  34  * @run testng Basic
  35  */
  36 
  37 import org.testng.Assert;
  38 import org.testng.annotations.AfterClass;
  39 import org.testng.annotations.Test;
  40 
  41 import java.io.ByteArrayInputStream;
  42 import java.io.ByteArrayOutputStream;
  43 import java.io.IOException;
  44 import java.io.PrintStream;
  45 import java.io.UncheckedIOException;
  46 import java.lang.module.ModuleDescriptor;
  47 import java.lang.module.ModuleDescriptor.Version;
  48 import java.nio.file.Files;
  49 import java.nio.file.Path;
  50 import java.nio.file.Paths;
  51 import java.util.Arrays;
  52 import java.util.Optional;
  53 import java.util.Set;
  54 import java.util.spi.ToolProvider;
  55 import java.util.stream.Collectors;
  56 import java.util.stream.Stream;
  57 import java.util.zip.ZipFile;
  58 
  59 import jdk.internal.module.ModuleInfoExtender;
  60 import jdk.testlibrary.FileUtils;
  61 
  62 public class Basic {
  63     private static final ToolProvider JAR_TOOL = ToolProvider.findFirst("jar")
  64            .orElseThrow(() -> new RuntimeException("jar tool not found"));
  65     private static final ToolProvider JAVAC_TOOL = ToolProvider.findFirst("javac")
  66             .orElseThrow(() -> new RuntimeException("javac tool not found"));
  67     private final String linesep = System.lineSeparator();
  68     private final Path testsrc;
  69     private final Path userdir;
  70     private final ByteArrayOutputStream outbytes = new ByteArrayOutputStream();
  71     private final PrintStream out = new PrintStream(outbytes, true);
  72     private final ByteArrayOutputStream errbytes = new ByteArrayOutputStream();
  73     private final PrintStream err = new PrintStream(errbytes, true);
  74 
  75     public Basic() throws IOException {
  76         testsrc = Paths.get(System.getProperty("test.src"));
  77         userdir = Paths.get(System.getProperty("user.dir", "."));
  78 
  79         // compile the classes directory
  80         Path source = testsrc.resolve("src").resolve("classes");
  81         Path destination = Paths.get("classes");
  82         javac(source, destination);
  83 
  84         // compile the mr9 directory including module-info.java
  85         source = testsrc.resolve("src").resolve("mr9");
  86         destination = Paths.get("mr9");
  87         javac(source, destination);
  88 
  89         // move module-info.class for later use
  90         Files.move(destination.resolve("module-info.class"),
  91                 Paths.get("module-info.class"));
  92     }
  93 
  94     private void javac(Path source, Path destination) throws IOException {
  95         String[] args = Stream.concat(
  96                 Stream.of("-d", destination.toString()),
  97                 Files.walk(source)
  98                         .map(Path::toString)
  99                         .filter(s -> s.endsWith(".java"))
 100         ).toArray(String[]::new);
 101         JAVAC_TOOL.run(System.out, System.err, args);
 102     }
 103 
 104     private int jar(String cmd) {
 105         outbytes.reset();
 106         errbytes.reset();
 107         return JAR_TOOL.run(out, err, cmd.split(" +"));
 108     }
 109 
 110     @AfterClass
 111     public void cleanup() throws IOException {
 112         Files.walk(userdir, 1)
 113                 .filter(p -> !p.equals(userdir))
 114                 .forEach(p -> {
 115                     try {
 116                         if (Files.isDirectory(p)) {
 117                             FileUtils.deleteFileTreeWithRetry(p);
 118                         } else {
 119                             FileUtils.deleteFileIfExistsWithRetry(p);
 120                         }
 121                     } catch (IOException x) {
 122                         throw new UncheckedIOException(x);
 123                     }
 124                 });
 125     }
 126 
 127     // updates a valid multi-release jar with a new public class in
 128     // versioned section and fails
 129     @Test
 130     public void test1() {
 131         // successful build of multi-release jar
 132         int rc = jar("-cf mmr.jar -C classes . --release 9 -C mr9 p/Hi.class");
 133         Assert.assertEquals(rc, 0);
 134 
 135         jar("-tf mmr.jar");
 136 
 137         Set<String> actual = lines(outbytes);
 138         Set<String> expected = Set.of(
 139                 "META-INF/",
 140                 "META-INF/MANIFEST.MF",
 141                 "p/",
 142                 "p/Hi.class",
 143                 "META-INF/versions/9/p/Hi.class"
 144         );
 145         Assert.assertEquals(actual, expected);
 146 
 147         // failed build because of new public class
 148         rc = jar("-uf mmr.jar --release 9 -C mr9 p/internal/Bar.class");
 149         Assert.assertEquals(rc, 1);
 150 
 151         String s = new String(errbytes.toByteArray());
 152         Assert.assertTrue(Message.NOT_FOUND_IN_BASE_ENTRY.match(s, "p/internal/Bar.class"));
 153     }
 154 
 155     // updates a valid multi-release jar with a module-info class and new
 156     // concealed public class in versioned section and succeeds
 157     @Test
 158     public void test2() {
 159         // successful build of multi-release jar
 160         int rc = jar("-cf mmr.jar -C classes . --release 9 -C mr9 p/Hi.class");
 161         Assert.assertEquals(rc, 0);
 162 
 163         // successful build because of module-info and new public class
 164         rc = jar("-uf mmr.jar module-info.class --release 9 -C mr9 p/internal/Bar.class");
 165         Assert.assertEquals(rc, 0);
 166 
 167         String s = new String(errbytes.toByteArray());
 168         Assert.assertTrue(Message.NEW_CONCEALED_PACKAGE_WARNING.match(s, "p/internal/Bar.class"));
 169 
 170         jar("-tf mmr.jar");
 171 
 172         Set<String> actual = lines(outbytes);
 173         Set<String> expected = Set.of(
 174                 "META-INF/",
 175                 "META-INF/MANIFEST.MF",
 176                 "p/",
 177                 "p/Hi.class",
 178                 "META-INF/versions/9/p/Hi.class",
 179                 "META-INF/versions/9/p/internal/Bar.class",
 180                 "module-info.class"
 181         );
 182         Assert.assertEquals(actual, expected);
 183     }
 184 
 185     // jar tool fails building mmr.jar because of new public class
 186     @Test
 187     public void test3() {
 188         int rc = jar("-cf mmr.jar -C classes . --release 9 -C mr9 .");
 189         Assert.assertEquals(rc, 1);
 190 
 191         String s = new String(errbytes.toByteArray());
 192         Assert.assertTrue(Message.NOT_FOUND_IN_BASE_ENTRY.match(s, "p/internal/Bar.class"));
 193     }
 194 
 195     // jar tool succeeds building mmr.jar because of concealed package
 196     @Test
 197     public void test4() {
 198         int rc = jar("-cf mmr.jar module-info.class -C classes . " +
 199                 "--release 9 module-info.class -C mr9 .");
 200         Assert.assertEquals(rc, 0);
 201 
 202         String s = new String(errbytes.toByteArray());
 203         Assert.assertTrue(Message.NEW_CONCEALED_PACKAGE_WARNING.match(s, "p/internal/Bar.class"));
 204 
 205         jar("-tf mmr.jar");
 206 
 207         Set<String> actual = lines(outbytes);
 208         Set<String> expected = Set.of(
 209                 "META-INF/",
 210                 "META-INF/MANIFEST.MF",
 211                 "module-info.class",
 212                 "META-INF/versions/9/module-info.class",
 213                 "p/",
 214                 "p/Hi.class",
 215                 "META-INF/versions/9/",
 216                 "META-INF/versions/9/p/",
 217                 "META-INF/versions/9/p/Hi.class",
 218                 "META-INF/versions/9/p/internal/",
 219                 "META-INF/versions/9/p/internal/Bar.class"
 220         );
 221         Assert.assertEquals(actual, expected);
 222     }
 223 
 224     // jar tool does two updates, no exported packages, all concealed
 225     @Test
 226     public void test5() throws IOException {
 227         // compile the mr10 directory
 228         Path source = testsrc.resolve("src").resolve("mr10");
 229         Path destination = Paths.get("mr10");
 230         javac(source, destination);
 231 
 232         // create a directory for this tests special files
 233         Files.createDirectory(Paths.get("test5"));
 234 
 235         // create an empty module-info.java
 236         String hi = "module hi {" + linesep + "}" + linesep;
 237         Path modinfo = Paths.get("test5", "module-info.java");
 238         Files.write(modinfo, hi.getBytes());
 239 
 240         // and compile it
 241         javac(modinfo, Paths.get("test5"));
 242 
 243         int rc = jar("--create --file mr.jar -C classes .");
 244         Assert.assertEquals(rc, 0);
 245 
 246         rc = jar("--update --file mr.jar -C test5 module-info.class"
 247                 + " --release 9 -C mr9 .");
 248         Assert.assertEquals(rc, 0);
 249 
 250         jar("tf mr.jar");
 251 
 252         Set<String> actual = lines(outbytes);
 253         Set<String> expected = Set.of(
 254                 "META-INF/",
 255                 "META-INF/MANIFEST.MF",
 256                 "p/",
 257                 "p/Hi.class",
 258                 "META-INF/versions/9/",
 259                 "META-INF/versions/9/p/",
 260                 "META-INF/versions/9/p/Hi.class",
 261                 "META-INF/versions/9/p/internal/",
 262                 "META-INF/versions/9/p/internal/Bar.class",
 263                 "module-info.class"
 264         );
 265         Assert.assertEquals(actual, expected);
 266 
 267         jar("-d --file mr.jar");
 268 
 269         actual = lines(outbytes);
 270         expected = Set.of(
 271                 "hi",
 272                 "requires mandated java.base",
 273                 "contains p",
 274                 "contains p.internal"
 275         );
 276         Assert.assertEquals(actual, expected);
 277 
 278         rc = jar("--update --file mr.jar --release 10 -C mr10 .");
 279         Assert.assertEquals(rc, 0);
 280 
 281         jar("tf mr.jar");
 282 
 283         actual = lines(outbytes);
 284         expected = Set.of(
 285                 "META-INF/",
 286                 "META-INF/MANIFEST.MF",
 287                 "p/",
 288                 "p/Hi.class",
 289                 "META-INF/versions/9/",
 290                 "META-INF/versions/9/p/",
 291                 "META-INF/versions/9/p/Hi.class",
 292                 "META-INF/versions/9/p/internal/",
 293                 "META-INF/versions/9/p/internal/Bar.class",
 294                 "META-INF/versions/10/",
 295                 "META-INF/versions/10/p/",
 296                 "META-INF/versions/10/p/internal/",
 297                 "META-INF/versions/10/p/internal/bar/",
 298                 "META-INF/versions/10/p/internal/bar/Gee.class",
 299                 "module-info.class"
 300         );
 301         Assert.assertEquals(actual, expected);
 302 
 303         jar("-d --file mr.jar");
 304 
 305         actual = lines(outbytes);
 306         expected = Set.of(
 307                 "hi",
 308                 "requires mandated java.base",
 309                 "contains p",
 310                 "contains p.internal",
 311                 "contains p.internal.bar"
 312         );
 313         Assert.assertEquals(actual, expected);
 314     }
 315 
 316     // root and versioned module-info entries have different main-class, version
 317     // attributes
 318     @Test
 319     public void test6() throws IOException {
 320         // create a directory for this tests special files
 321         Files.createDirectory(Paths.get("test6"));
 322         Files.createDirectory(Paths.get("test6-v9"));
 323 
 324         // compile the classes directory
 325         Path src = testsrc.resolve("src").resolve("classes");
 326         Path dst = Paths.get("test6");
 327         javac(src, dst);
 328 
 329         byte[] mdBytes = Files.readAllBytes(Paths.get("module-info.class"));
 330 
 331         ModuleInfoExtender mie = ModuleInfoExtender.newExtender(
 332             new ByteArrayInputStream(mdBytes));
 333 
 334         mie.mainClass("foo.main");
 335         mie.version(Version.parse("1.0"));
 336 
 337         ByteArrayOutputStream baos = new ByteArrayOutputStream();
 338         mie.write(baos);
 339         Files.write(Paths.get("test6", "module-info.class"), baos.toByteArray());
 340         Files.write(Paths.get("test6-v9", "module-info.class"), baos.toByteArray());
 341 
 342         int rc = jar("--create --file mmr.jar -C test6 . --release 9 -C test6-v9 .");
 343         Assert.assertEquals(rc, 0);
 344 
 345 
 346         // different main-class
 347         mie = ModuleInfoExtender.newExtender(new ByteArrayInputStream(mdBytes));
 348         mie.mainClass("foo.main2");
 349         mie.version(Version.parse("1.0"));
 350         baos.reset();
 351         mie.write(baos);
 352         Files.write(Paths.get("test6-v9", "module-info.class"), baos.toByteArray());
 353 
 354         rc = jar("--create --file mmr.jar -C test6 . --release 9 -C test6-v9 .");
 355         Assert.assertEquals(rc, 1);
 356 
 357         Assert.assertTrue(Message.CONTAINS_DIFFERENT_MAINCLASS.match(
 358             new String(errbytes.toByteArray()),
 359             "META-INF/versions/9/module-info.class"));
 360 
 361         // different version
 362         mie = ModuleInfoExtender.newExtender(new ByteArrayInputStream(mdBytes));
 363         mie.mainClass("foo.main");
 364         mie.version(Version.parse("2.0"));
 365         baos.reset();
 366         mie.write(baos);
 367         Files.write(Paths.get("test6-v9", "module-info.class"), baos.toByteArray());
 368 
 369         rc = jar("--create --file mmr.jar -C test6 . --release 9 -C test6-v9 .");
 370         Assert.assertEquals(rc, 1);
 371 
 372         Assert.assertTrue(Message.CONTAINS_DIFFERENT_VERSION.match(
 373             new String(errbytes.toByteArray()),
 374             "META-INF/versions/9/module-info.class"));
 375 
 376     }
 377 
 378     // versioned mmr without root module-info.class
 379     @Test
 380     public void test7() throws IOException {
 381         // create a directory for this tests special files
 382         Files.createDirectory(Paths.get("test7"));
 383         Files.createDirectory(Paths.get("test7-v9"));
 384         Files.createDirectory(Paths.get("test7-v10"));
 385 
 386         // compile the classes directory
 387         Path src = testsrc.resolve("src").resolve("classes");
 388         Path dst = Paths.get("test7");
 389         javac(src, dst);
 390 
 391         // move module-info.class to v9 later use
 392         Files.copy(Paths.get("module-info.class"),
 393                    Paths.get("test7-v9", "module-info.class"));
 394 
 395         Files.copy(Paths.get("test7-v9", "module-info.class"),
 396                    Paths.get("test7-v10", "module-info.class"));
 397 
 398         int rc = jar("--create --file mmr.jar --main-class=foo.main -C test7 . --release 9 -C test7-v9 . --release 10 -C test7-v10 .");
 399 
 400 System.out.println("-----------------------");
 401 System.out.println( new String(errbytes.toByteArray()));
 402 
 403 
 404         Assert.assertEquals(rc, 0);
 405 
 406 
 407         jar("-tf mmr.jar");
 408 
 409 System.out.println("-----------------------");
 410 System.out.println( new String(outbytes.toByteArray()));
 411 
 412         Optional<String> exp = Optional.of("foo.main");
 413         try (ZipFile zf = new ZipFile("mmr.jar")) {
 414             Assert.assertTrue(zf.getEntry("module-info.class") == null);
 415 
 416             ModuleDescriptor md = ModuleDescriptor.read(
 417                 zf.getInputStream(zf.getEntry("META-INF/versions/9/module-info.class")));
 418             Assert.assertEquals(md.mainClass(), exp);
 419 
 420             md = ModuleDescriptor.read(
 421                 zf.getInputStream(zf.getEntry("META-INF/versions/10/module-info.class")));
 422             Assert.assertEquals(md.mainClass(), exp);
 423         }
 424     }
 425 
 426     private static Set<String> lines(ByteArrayOutputStream baos) {
 427         String s = new String(baos.toByteArray());
 428         return Arrays.stream(s.split("\\R"))
 429                      .map(l -> l.trim())
 430                      .filter(l -> l.length() > 0)
 431                      .collect(Collectors.toSet());
 432     }
 433 
 434     static enum Message {
 435         CONTAINS_DIFFERENT_MAINCLASS(
 436           ": module-info.class in a versioned directory contains different \"main-class\""
 437         ),
 438         CONTAINS_DIFFERENT_VERSION(
 439           ": module-info.class in a versioned directory contains different \"version\""
 440         ),
 441         NOT_FOUND_IN_BASE_ENTRY(
 442           ", contains a new public class not found in base entries"
 443         ),
 444         NEW_CONCEALED_PACKAGE_WARNING(
 445             " is a public class" +
 446             " in a concealed package, placing this jar on the class path will result" +
 447             " in incompatible public interfaces"
 448         );
 449 
 450         final String msg;
 451         Message(String msg) {
 452             this.msg = msg;
 453         }
 454 
 455         /*
 456          * Test if the given output contains this message ignoring the line break.
 457          */
 458         boolean match(String output, String entry) {
 459             System.out.println("Expected: " + entry + msg);
 460             System.out.println("Found: " + output);
 461             return Arrays.stream(output.split("\\R"))
 462                          .collect(Collectors.joining(" "))
 463                          .contains(entry + msg);
 464         }
 465     }
 466 }