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 }