1 /* 2 * Copyright (c) 2015, 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 import java.io.*; 25 import java.lang.reflect.Method; 26 import java.nio.file.Files; 27 import java.nio.file.Path; 28 import java.nio.file.Paths; 29 import java.util.ArrayList; 30 import java.util.Arrays; 31 import java.util.List; 32 import java.util.function.Consumer; 33 import java.util.jar.JarEntry; 34 import java.util.jar.JarInputStream; 35 import java.util.jar.JarOutputStream; 36 import java.util.stream.Stream; 37 38 import jdk.test.lib.util.FileUtils; 39 import jdk.testlibrary.JDKToolFinder; 40 import org.testng.annotations.BeforeTest; 41 import org.testng.annotations.Test; 42 43 import static java.lang.String.format; 44 import static java.lang.System.out; 45 import static java.nio.charset.StandardCharsets.UTF_8; 46 import static org.testng.Assert.assertFalse; 47 import static org.testng.Assert.assertTrue; 48 49 /* 50 * @test 51 * @bug 8170952 52 * @library /lib/testlibrary /test/lib 53 * @build jdk.test.lib.util.FileUtils 54 * jdk.test.lib.Utils 55 * jdk.test.lib.Asserts 56 * jdk.test.lib.JDKToolFinder 57 * jdk.test.lib.JDKToolLauncher 58 * jdk.test.lib.Platform 59 * jdk.test.lib.process.* 60 * jdk.testlibrary.JDKToolFinder 61 * @run testng CLICompatibility 62 * @summary Basic test for compatibility of CLI options 63 */ 64 65 public class CLICompatibility { 66 static final Path TEST_CLASSES = Paths.get(System.getProperty("test.classes", ".")); 67 static final Path USER_DIR = Paths.get(System.getProperty("user.dir")); 68 69 static final String TOOL_VM_OPTIONS = System.getProperty("test.tool.vm.opts", ""); 70 71 final boolean legacyOnly; // for running on older JDK's ( test validation ) 72 73 // Resources we know to exist, that can be used for creating jar files. 74 static final String RES1 = "CLICompatibility.class"; 75 static final String RES2 = "CLICompatibility$Result.class"; 76 77 @BeforeTest 78 public void setupResourcesForJar() throws Exception { 79 // Copy the files that we are going to use for creating/updating test 80 // jar files, so that they can be referred to without '-C dir' 81 Files.copy(TEST_CLASSES.resolve(RES1), USER_DIR.resolve(RES1)); 82 Files.copy(TEST_CLASSES.resolve(RES2), USER_DIR.resolve(RES2)); 83 } 84 85 static final IOConsumer<InputStream> ASSERT_CONTAINS_RES1 = in -> { 86 try (JarInputStream jin = new JarInputStream(in)) { 87 assertTrue(jarContains(jin, RES1), "Failed to find " + RES1); 88 } 89 }; 90 static final IOConsumer<InputStream> ASSERT_CONTAINS_RES2 = in -> { 91 try (JarInputStream jin = new JarInputStream(in)) { 92 assertTrue(jarContains(jin, RES2), "Failed to find " + RES2); 93 } 94 }; 95 static final IOConsumer<InputStream> ASSERT_CONTAINS_MAINFEST = in -> { 96 try (JarInputStream jin = new JarInputStream(in)) { 97 assertTrue(jin.getManifest() != null, "No META-INF/MANIFEST.MF"); 98 } 99 }; 100 static final IOConsumer<InputStream> ASSERT_DOES_NOT_CONTAIN_MAINFEST = in -> { 101 try (JarInputStream jin = new JarInputStream(in)) { 102 assertTrue(jin.getManifest() == null, "Found unexpected META-INF/MANIFEST.MF"); 103 } 104 }; 105 106 static final FailCheckerWithMessage FAIL_TOO_MANY_MAIN_OPS = 107 new FailCheckerWithMessage("You may not specify more than one '-cuxtid' options", 108 /* legacy */ "{ctxui}[vfmn0Me] [jar-file] [manifest-file] [entry-point] [-C dir] files"); 109 110 // Create 111 112 @Test 113 public void createBadArgs() { 114 final FailCheckerWithMessage FAIL_CREATE_NO_ARGS = new FailCheckerWithMessage( 115 "'c' flag requires manifest or input files to be specified!"); 116 117 jar("c") 118 .assertFailure() 119 .resultChecker(FAIL_CREATE_NO_ARGS); 120 121 jar("-c") 122 .assertFailure() 123 .resultChecker(FAIL_CREATE_NO_ARGS); 124 125 if (!legacyOnly) 126 jar("--create") 127 .assertFailure() 128 .resultChecker(FAIL_CREATE_NO_ARGS); 129 130 jar("ct") 131 .assertFailure() 132 .resultChecker(FAIL_TOO_MANY_MAIN_OPS); 133 134 jar("-ct") 135 .assertFailure() 136 .resultChecker(FAIL_TOO_MANY_MAIN_OPS); 137 138 if (!legacyOnly) 139 jar("--create --list") 140 .assertFailure() 141 .resultChecker(FAIL_TOO_MANY_MAIN_OPS); 142 } 143 144 @Test 145 public void createWriteToFile() throws IOException { 146 Path path = Paths.get("createJarFile.jar"); // for creating 147 String jn = path.toString(); 148 for (String opts : new String[]{"cf " + jn, "-cf " + jn, "--create --file=" + jn}) { 149 if (legacyOnly && opts.startsWith("--")) 150 continue; 151 152 jar(opts, RES1) 153 .assertSuccess() 154 .resultChecker(r -> { 155 ASSERT_CONTAINS_RES1.accept(Files.newInputStream(path)); 156 ASSERT_CONTAINS_MAINFEST.accept(Files.newInputStream(path)); 157 }); 158 } 159 FileUtils.deleteFileIfExistsWithRetry(path); 160 } 161 162 @Test 163 public void createWriteToStdout() throws IOException { 164 for (String opts : new String[]{"c", "-c", "--create"}) { 165 if (legacyOnly && opts.startsWith("--")) 166 continue; 167 168 jar(opts, RES1) 169 .assertSuccess() 170 .resultChecker(r -> { 171 ASSERT_CONTAINS_RES1.accept(r.stdoutAsStream()); 172 ASSERT_CONTAINS_MAINFEST.accept(r.stdoutAsStream()); 173 }); 174 } 175 } 176 177 @Test 178 public void createWriteToStdoutNoManifest() throws IOException { 179 for (String opts : new String[]{"cM", "-cM", "--create --no-manifest"} ){ 180 if (legacyOnly && opts.startsWith("--")) 181 continue; 182 183 jar(opts, RES1) 184 .assertSuccess() 185 .resultChecker(r -> { 186 ASSERT_CONTAINS_RES1.accept(r.stdoutAsStream()); 187 ASSERT_DOES_NOT_CONTAIN_MAINFEST.accept(r.stdoutAsStream()); 188 }); 189 } 190 } 191 192 // Update 193 194 @Test 195 public void updateBadArgs() { 196 final FailCheckerWithMessage FAIL_UPDATE_NO_ARGS = new FailCheckerWithMessage( 197 "'u' flag requires manifest, 'e' flag or input files to be specified!"); 198 199 jar("u") 200 .assertFailure() 201 .resultChecker(FAIL_UPDATE_NO_ARGS); 202 203 jar("-u") 204 .assertFailure() 205 .resultChecker(FAIL_UPDATE_NO_ARGS); 206 207 if (!legacyOnly) 208 jar("--update") 209 .assertFailure() 210 .resultChecker(FAIL_UPDATE_NO_ARGS); 211 212 jar("ut") 213 .assertFailure() 214 .resultChecker(FAIL_TOO_MANY_MAIN_OPS); 215 216 jar("-ut") 217 .assertFailure() 218 .resultChecker(FAIL_TOO_MANY_MAIN_OPS); 219 220 if (!legacyOnly) 221 jar("--update --list") 222 .assertFailure() 223 .resultChecker(FAIL_TOO_MANY_MAIN_OPS); 224 } 225 226 @Test 227 public void updateReadFileWriteFile() throws IOException { 228 Path path = Paths.get("updateReadWriteStdout.jar"); // for updating 229 String jn = path.toString(); 230 231 for (String opts : new String[]{"uf " + jn, "-uf " + jn, "--update --file=" + jn}) { 232 if (legacyOnly && opts.startsWith("--")) 233 continue; 234 235 createJar(path, RES1); 236 jar(opts, RES2) 237 .assertSuccess() 238 .resultChecker(r -> { 239 ASSERT_CONTAINS_RES1.accept(Files.newInputStream(path)); 240 ASSERT_CONTAINS_RES2.accept(Files.newInputStream(path)); 241 ASSERT_CONTAINS_MAINFEST.accept(Files.newInputStream(path)); 242 }); 243 } 244 FileUtils.deleteFileIfExistsWithRetry(path); 245 } 246 247 @Test 248 public void updateReadStdinWriteStdout() throws IOException { 249 Path path = Paths.get("updateReadStdinWriteStdout.jar"); 250 251 for (String opts : new String[]{"u", "-u", "--update"}) { 252 if (legacyOnly && opts.startsWith("--")) 253 continue; 254 255 createJar(path, RES1); 256 jarWithStdin(path.toFile(), opts, RES2) 257 .assertSuccess() 258 .resultChecker(r -> { 259 ASSERT_CONTAINS_RES1.accept(r.stdoutAsStream()); 260 ASSERT_CONTAINS_RES2.accept(r.stdoutAsStream()); 261 ASSERT_CONTAINS_MAINFEST.accept(r.stdoutAsStream()); 262 }); 263 } 264 FileUtils.deleteFileIfExistsWithRetry(path); 265 } 266 267 @Test 268 public void updateReadStdinWriteStdoutNoManifest() throws IOException { 269 Path path = Paths.get("updateReadStdinWriteStdoutNoManifest.jar"); 270 271 for (String opts : new String[]{"uM", "-uM", "--update --no-manifest"} ){ 272 if (legacyOnly && opts.startsWith("--")) 273 continue; 274 275 createJar(path, RES1); 276 jarWithStdin(path.toFile(), opts, RES2) 277 .assertSuccess() 278 .resultChecker(r -> { 279 ASSERT_CONTAINS_RES1.accept(r.stdoutAsStream()); 280 ASSERT_CONTAINS_RES2.accept(r.stdoutAsStream()); 281 ASSERT_DOES_NOT_CONTAIN_MAINFEST.accept(r.stdoutAsStream()); 282 }); 283 } 284 FileUtils.deleteFileIfExistsWithRetry(path); 285 } 286 287 // List 288 289 @Test 290 public void listBadArgs() { 291 jar("tx") 292 .assertFailure() 293 .resultChecker(FAIL_TOO_MANY_MAIN_OPS); 294 295 jar("-tx") 296 .assertFailure() 297 .resultChecker(FAIL_TOO_MANY_MAIN_OPS); 298 299 if (!legacyOnly) 300 jar("--list --extract") 301 .assertFailure() 302 .resultChecker(FAIL_TOO_MANY_MAIN_OPS); 303 } 304 305 @Test 306 public void listReadFromFileWriteToStdout() throws IOException { 307 Path path = Paths.get("listReadFromFileWriteToStdout.jar"); // for listing 308 createJar(path, RES1); 309 String jn = path.toString(); 310 311 for (String opts : new String[]{"tf " + jn, "-tf " + jn, "--list --file " + jn}) { 312 if (legacyOnly && opts.startsWith("--")) 313 continue; 314 315 jar(opts) 316 .assertSuccess() 317 .resultChecker(r -> 318 assertTrue(r.output.contains("META-INF/MANIFEST.MF") && r.output.contains(RES1), 319 "Failed, got [" + r.output + "]") 320 ); 321 } 322 FileUtils.deleteFileIfExistsWithRetry(path); 323 } 324 325 @Test 326 public void listReadFromStdinWriteToStdout() throws IOException { 327 Path path = Paths.get("listReadFromStdinWriteToStdout.jar"); 328 createJar(path, RES1); 329 330 for (String opts : new String[]{"t", "-t", "--list"} ){ 331 if (legacyOnly && opts.startsWith("--")) 332 continue; 333 334 jarWithStdin(path.toFile(), opts) 335 .assertSuccess() 336 .resultChecker(r -> 337 assertTrue(r.output.contains("META-INF/MANIFEST.MF") && r.output.contains(RES1), 338 "Failed, got [" + r.output + "]") 339 ); 340 } 341 FileUtils.deleteFileIfExistsWithRetry(path); 342 } 343 344 // Extract 345 346 @Test 347 public void extractBadArgs() { 348 jar("xi") 349 .assertFailure() 350 .resultChecker(FAIL_TOO_MANY_MAIN_OPS); 351 352 jar("-xi") 353 .assertFailure() 354 .resultChecker(FAIL_TOO_MANY_MAIN_OPS); 355 356 if (!legacyOnly) { 357 jar("--extract --generate-index") 358 .assertFailure() 359 .resultChecker(new FailCheckerWithMessage( 360 "option --generate-index requires an argument")); 361 362 jar("--extract --generate-index=foo") 363 .assertFailure() 364 .resultChecker(FAIL_TOO_MANY_MAIN_OPS); 365 } 366 } 367 368 @Test 369 public void extractReadFromStdin() throws IOException { 370 Path path = Paths.get("extract"); 371 Path jarPath = path.resolve("extractReadFromStdin.jar"); // for extracting 372 createJar(jarPath, RES1); 373 374 for (String opts : new String[]{"x" ,"-x", "--extract"}) { 375 if (legacyOnly && opts.startsWith("--")) 376 continue; 377 378 jarWithStdinAndWorkingDir(jarPath.toFile(), path.toFile(), opts) 379 .assertSuccess() 380 .resultChecker(r -> 381 assertTrue(Files.exists(path.resolve(RES1)), 382 "Expected to find:" + path.resolve(RES1)) 383 ); 384 FileUtils.deleteFileIfExistsWithRetry(path.resolve(RES1)); 385 } 386 FileUtils.deleteFileTreeWithRetry(path); 387 } 388 389 @Test 390 public void extractReadFromFile() throws IOException { 391 Path path = Paths.get("extract"); 392 String jn = "extractReadFromFile.jar"; 393 Path jarPath = path.resolve(jn); 394 createJar(jarPath, RES1); 395 396 for (String opts : new String[]{"xf "+jn ,"-xf "+jn, "--extract --file "+jn}) { 397 if (legacyOnly && opts.startsWith("--")) 398 continue; 399 400 jarWithStdinAndWorkingDir(null, path.toFile(), opts) 401 .assertSuccess() 402 .resultChecker(r -> 403 assertTrue(Files.exists(path.resolve(RES1)), 404 "Expected to find:" + path.resolve(RES1)) 405 ); 406 FileUtils.deleteFileIfExistsWithRetry(path.resolve(RES1)); 407 } 408 FileUtils.deleteFileTreeWithRetry(path); 409 } 410 411 // Basic help 412 413 @Test 414 public void helpBadOptionalArg() { 415 if (legacyOnly) 416 return; 417 418 jar("--help:") 419 .assertFailure(); 420 421 jar("--help:blah") 422 .assertFailure(); 423 } 424 425 @Test 426 public void help() { 427 if (legacyOnly) 428 return; 429 430 jar("-h") 431 .assertSuccess() 432 .resultChecker(r -> 433 assertTrue(r.output.startsWith("Usage: jar [OPTION...] [ [--release VERSION] [-C dir] files]"), 434 "Failed, got [" + r.output + "]") 435 ); 436 437 jar("--help") 438 .assertSuccess() 439 .resultChecker(r -> { 440 assertTrue(r.output.startsWith("Usage: jar [OPTION...] [ [--release VERSION] [-C dir] files]"), 441 "Failed, got [" + r.output + "]"); 442 assertFalse(r.output.contains("--do-not-resolve-by-default")); 443 assertFalse(r.output.contains("--warn-if-resolved")); 444 }); 445 446 jar("--help:compat") 447 .assertSuccess() 448 .resultChecker(r -> 449 assertTrue(r.output.startsWith("Compatibility Interface:"), 450 "Failed, got [" + r.output + "]") 451 ); 452 453 jar("--help-extra") 454 .assertSuccess() 455 .resultChecker(r -> { 456 assertTrue(r.output.startsWith("Usage: jar [OPTION...] [ [--release VERSION] [-C dir] files]"), 457 "Failed, got [" + r.output + "]"); 458 assertTrue(r.output.contains("--do-not-resolve-by-default")); 459 assertTrue(r.output.contains("--warn-if-resolved")); 460 }); 461 } 462 463 // -- Infrastructure 464 465 static boolean jarContains(JarInputStream jis, String entryName) 466 throws IOException 467 { 468 JarEntry e; 469 boolean found = false; 470 while((e = jis.getNextJarEntry()) != null) { 471 if (e.getName().equals(entryName)) 472 return true; 473 } 474 return false; 475 } 476 477 /* Creates a simple jar with entries of size 0, good enough for testing */ 478 static void createJar(Path path, String... entries) throws IOException { 479 FileUtils.deleteFileIfExistsWithRetry(path); 480 Path parent = path.getParent(); 481 if (parent != null) 482 Files.createDirectories(parent); 483 try (OutputStream out = Files.newOutputStream(path); 484 JarOutputStream jos = new JarOutputStream(out)) { 485 JarEntry je = new JarEntry("META-INF/MANIFEST.MF"); 486 jos.putNextEntry(je); 487 jos.closeEntry(); 488 489 for (String entry : entries) { 490 je = new JarEntry(entry); 491 jos.putNextEntry(je); 492 jos.closeEntry(); 493 } 494 } 495 } 496 497 static class FailCheckerWithMessage implements Consumer<Result> { 498 final String[] messages; 499 FailCheckerWithMessage(String... m) { 500 messages = m; 501 } 502 @Override 503 public void accept(Result r) { 504 //out.printf("%s%n", r.output); 505 boolean found = false; 506 for (String m : messages) { 507 if (r.output.contains(m)) { 508 found = true; 509 break; 510 } 511 } 512 assertTrue(found, 513 "Excepted out to contain one of: " + Arrays.asList(messages) 514 + " but got: " + r.output); 515 } 516 } 517 518 static Result jar(String... args) { 519 return jarWithStdinAndWorkingDir(null, null, args); 520 } 521 522 static Result jarWithStdin(File stdinSource, String... args) { 523 return jarWithStdinAndWorkingDir(stdinSource, null, args); 524 } 525 526 static Result jarWithStdinAndWorkingDir(File stdinFrom, 527 File workingDir, 528 String... args) { 529 String jar = getJDKTool("jar"); 530 List<String> commands = new ArrayList<>(); 531 commands.add(jar); 532 if (!TOOL_VM_OPTIONS.isEmpty()) { 533 commands.addAll(Arrays.asList(TOOL_VM_OPTIONS.split("\\s+", -1))); 534 } 535 Stream.of(args).map(s -> s.split(" ")) 536 .flatMap(Arrays::stream) 537 .forEach(x -> commands.add(x)); 538 ProcessBuilder p = new ProcessBuilder(commands); 539 if (stdinFrom != null) 540 p.redirectInput(stdinFrom); 541 if (workingDir != null) 542 p.directory(workingDir); 543 return run(p); 544 } 545 546 static Result run(ProcessBuilder pb) { 547 Process p; 548 byte[] stdout, stderr; 549 out.printf("Running: %s%n", pb.command()); 550 try { 551 p = pb.start(); 552 } catch (IOException e) { 553 throw new RuntimeException( 554 format("Couldn't start process '%s'", pb.command()), e); 555 } 556 557 String output; 558 try { 559 stdout = readAllBytes(p.getInputStream()); 560 stderr = readAllBytes(p.getErrorStream()); 561 562 output = toString(stdout, stderr); 563 } catch (IOException e) { 564 throw new RuntimeException( 565 format("Couldn't read process output '%s'", pb.command()), e); 566 } 567 568 try { 569 p.waitFor(); 570 } catch (InterruptedException e) { 571 throw new RuntimeException( 572 format("Process hasn't finished '%s'", pb.command()), e); 573 } 574 return new Result(p.exitValue(), stdout, stderr, output); 575 } 576 577 static final Path JAVA_HOME = Paths.get(System.getProperty("java.home")); 578 579 static String getJDKTool(String name) { 580 try { 581 return JDKToolFinder.getJDKTool(name); 582 } catch (Exception x) { 583 Path j = JAVA_HOME.resolve("bin").resolve(name); 584 if (Files.exists(j)) 585 return j.toString(); 586 j = JAVA_HOME.resolve("..").resolve("bin").resolve(name); 587 if (Files.exists(j)) 588 return j.toString(); 589 throw new RuntimeException(x); 590 } 591 } 592 593 static String toString(byte[] ba1, byte[] ba2) { 594 return (new String(ba1, UTF_8)).concat(new String(ba2, UTF_8)); 595 } 596 597 static class Result { 598 final int exitValue; 599 final byte[] stdout; 600 final byte[] stderr; 601 final String output; 602 603 private Result(int exitValue, byte[] stdout, byte[] stderr, String output) { 604 this.exitValue = exitValue; 605 this.stdout = stdout; 606 this.stderr = stderr; 607 this.output = output; 608 } 609 610 InputStream stdoutAsStream() { return new ByteArrayInputStream(stdout); } 611 612 Result assertSuccess() { assertTrue(exitValue == 0, output); return this; } 613 Result assertFailure() { assertTrue(exitValue != 0, output); return this; } 614 615 Result resultChecker(IOConsumer<Result> r) { 616 try { r.accept(this); return this; } 617 catch (IOException x) { throw new UncheckedIOException(x); } 618 } 619 620 Result resultChecker(FailCheckerWithMessage c) { c.accept(this); return this; } 621 } 622 623 interface IOConsumer<T> { void accept(T t) throws IOException ; } 624 625 // readAllBytes implementation so the test can be run pre 1.9 ( legacyOnly ) 626 static byte[] readAllBytes(InputStream is) throws IOException { 627 byte[] buf = new byte[8192]; 628 int capacity = buf.length; 629 int nread = 0; 630 int n; 631 for (;;) { 632 // read to EOF which may read more or less than initial buffer size 633 while ((n = is.read(buf, nread, capacity - nread)) > 0) 634 nread += n; 635 636 // if the last call to read returned -1, then we're done 637 if (n < 0) 638 break; 639 640 // need to allocate a larger buffer 641 capacity = capacity << 1; 642 643 buf = Arrays.copyOf(buf, capacity); 644 } 645 return (capacity == nread) ? buf : Arrays.copyOf(buf, nread); 646 } 647 648 // Standalone entry point for running with, possibly older, JDKs. 649 public static void main(String[] args) throws Throwable { 650 boolean legacyOnly = false; 651 if (args.length != 0 && args[0].equals("legacyOnly")) 652 legacyOnly = true; 653 654 CLICompatibility test = new CLICompatibility(legacyOnly); 655 for (Method m : CLICompatibility.class.getDeclaredMethods()) { 656 if (m.getAnnotation(Test.class) != null) { 657 System.out.println("Invoking " + m.getName()); 658 m.invoke(test); 659 } 660 } 661 } 662 CLICompatibility(boolean legacyOnly) { this.legacyOnly = legacyOnly; } 663 CLICompatibility() { this.legacyOnly = false; } 664 }