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
24import java.io.*;
25import java.lang.reflect.Method;
26import java.nio.file.Files;
27import java.nio.file.Path;
28import java.nio.file.Paths;
29import java.util.ArrayList;
30import java.util.Arrays;
31import java.util.List;
32import java.util.function.Consumer;
33import java.util.jar.JarEntry;
34import java.util.jar.JarInputStream;
35import java.util.jar.JarOutputStream;
36import java.util.stream.Stream;
37
38import jdk.testlibrary.FileUtils;
39import jdk.testlibrary.JDKToolFinder;
40import org.testng.annotations.BeforeTest;
41import org.testng.annotations.Test;
42
43import static java.lang.String.format;
44import static java.lang.System.out;
45import static java.nio.charset.StandardCharsets.UTF_8;
46import static org.testng.Assert.assertFalse;
47import static org.testng.Assert.assertTrue;
48
49/*
50 * @test
51 * @bug 8170952
52 * @library /lib/testlibrary
53 * @build jdk.testlibrary.FileUtils jdk.testlibrary.JDKToolFinder
54 * @run testng CLICompatibility
55 * @summary Basic test for compatibility of CLI options
56 */
57
58public 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}
658