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