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