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