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