jdk/test/tools/jar/compat/CLICompatibility.java
author alanb
Fri, 16 Dec 2016 06:19:16 +0000
changeset 42703 20c39ea4a507
parent 42463 39c0bbf9d03c
child 45286 cd809e28c082
permissions -rw-r--r--
8170987: Module system implementation refresh (12/2016) 8170859: Run time and tool support for ModuleResolution Reviewed-by: redestad, mchung, alanb Contributed-by: alan.bateman@oracle.com, mandy.chung@oracle.com, chris.hegarty@oracle.com, mark.reinhold@oracle.com, john.r.rose@oracle.com

/*
 * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

import java.io.*;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
import java.util.jar.JarOutputStream;
import java.util.stream.Stream;

import jdk.testlibrary.FileUtils;
import jdk.testlibrary.JDKToolFinder;
import org.testng.annotations.BeforeTest;
import org.testng.annotations.Test;

import static java.lang.String.format;
import static java.lang.System.out;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertTrue;

/*
 * @test
 * @bug 8170952
 * @library /lib/testlibrary
 * @build jdk.testlibrary.FileUtils jdk.testlibrary.JDKToolFinder
 * @run testng CLICompatibility
 * @summary Basic test for compatibility of CLI options
 */

public class CLICompatibility {
    static final Path TEST_CLASSES = Paths.get(System.getProperty("test.classes", "."));
    static final Path USER_DIR = Paths.get(System.getProperty("user.dir"));

    static final String TOOL_VM_OPTIONS = System.getProperty("test.tool.vm.opts", "");

    final boolean legacyOnly;  // for running on older JDK's ( test validation )

    // Resources we know to exist, that can be used for creating jar files.
    static final String RES1 = "CLICompatibility.class";
    static final String RES2 = "CLICompatibility$Result.class";

    @BeforeTest
    public void setupResourcesForJar() throws Exception {
        // Copy the files that we are going to use for creating/updating test
        // jar files, so that they can be referred to without '-C dir'
        Files.copy(TEST_CLASSES.resolve(RES1), USER_DIR.resolve(RES1));
        Files.copy(TEST_CLASSES.resolve(RES2), USER_DIR.resolve(RES2));
    }

    static final IOConsumer<InputStream> ASSERT_CONTAINS_RES1 = in -> {
        try (JarInputStream jin = new JarInputStream(in)) {
            assertTrue(jarContains(jin, RES1), "Failed to find " + RES1);
        }
    };
    static final IOConsumer<InputStream> ASSERT_CONTAINS_RES2 = in -> {
        try (JarInputStream jin = new JarInputStream(in)) {
            assertTrue(jarContains(jin, RES2), "Failed to find " + RES2);
        }
    };
    static final IOConsumer<InputStream> ASSERT_CONTAINS_MAINFEST = in -> {
        try (JarInputStream jin = new JarInputStream(in)) {
            assertTrue(jin.getManifest() != null, "No META-INF/MANIFEST.MF");
        }
    };
    static final IOConsumer<InputStream> ASSERT_DOES_NOT_CONTAIN_MAINFEST = in -> {
        try (JarInputStream jin = new JarInputStream(in)) {
            assertTrue(jin.getManifest() == null, "Found unexpected META-INF/MANIFEST.MF");
        }
    };

    static final FailCheckerWithMessage FAIL_TOO_MANY_MAIN_OPS =
        new FailCheckerWithMessage("You may not specify more than one '-cuxtid' options",
        /* legacy */ "{ctxui}[vfmn0Me] [jar-file] [manifest-file] [entry-point] [-C dir] files");

    // Create

    @Test
    public void createBadArgs() {
        final FailCheckerWithMessage FAIL_CREATE_NO_ARGS = new FailCheckerWithMessage(
                "'c' flag requires manifest or input files to be specified!");

        jar("c")
            .assertFailure()
            .resultChecker(FAIL_CREATE_NO_ARGS);

        jar("-c")
            .assertFailure()
            .resultChecker(FAIL_CREATE_NO_ARGS);

        if (!legacyOnly)
            jar("--create")
                .assertFailure()
                .resultChecker(FAIL_CREATE_NO_ARGS);

        jar("ct")
            .assertFailure()
            .resultChecker(FAIL_TOO_MANY_MAIN_OPS);

        jar("-ct")
            .assertFailure()
            .resultChecker(FAIL_TOO_MANY_MAIN_OPS);

        if (!legacyOnly)
            jar("--create --list")
                .assertFailure()
                .resultChecker(FAIL_TOO_MANY_MAIN_OPS);
    }

    @Test
    public void createWriteToFile() throws IOException {
        Path path = Paths.get("createJarFile.jar");  // for creating
        String jn = path.toString();
        for (String opts : new String[]{"cf " + jn, "-cf " + jn, "--create --file=" + jn}) {
            if (legacyOnly && opts.startsWith("--"))
                continue;

            jar(opts, RES1)
                .assertSuccess()
                .resultChecker(r -> {
                    ASSERT_CONTAINS_RES1.accept(Files.newInputStream(path));
                    ASSERT_CONTAINS_MAINFEST.accept(Files.newInputStream(path));
                });
        }
        FileUtils.deleteFileIfExistsWithRetry(path);
    }

    @Test
    public void createWriteToStdout() throws IOException {
        for (String opts : new String[]{"c", "-c", "--create"}) {
            if (legacyOnly && opts.startsWith("--"))
                continue;

            jar(opts, RES1)
                .assertSuccess()
                .resultChecker(r -> {
                    ASSERT_CONTAINS_RES1.accept(r.stdoutAsStream());
                    ASSERT_CONTAINS_MAINFEST.accept(r.stdoutAsStream());
                });
        }
    }

    @Test
    public void createWriteToStdoutNoManifest() throws IOException {
        for (String opts : new String[]{"cM", "-cM", "--create --no-manifest"} ){
            if (legacyOnly && opts.startsWith("--"))
                continue;

            jar(opts, RES1)
                .assertSuccess()
                .resultChecker(r -> {
                    ASSERT_CONTAINS_RES1.accept(r.stdoutAsStream());
                    ASSERT_DOES_NOT_CONTAIN_MAINFEST.accept(r.stdoutAsStream());
                });
        }
    }

    // Update

    @Test
    public void updateBadArgs() {
        final FailCheckerWithMessage FAIL_UPDATE_NO_ARGS = new FailCheckerWithMessage(
                "'u' flag requires manifest, 'e' flag or input files to be specified!");

        jar("u")
            .assertFailure()
            .resultChecker(FAIL_UPDATE_NO_ARGS);

        jar("-u")
            .assertFailure()
            .resultChecker(FAIL_UPDATE_NO_ARGS);

        if (!legacyOnly)
            jar("--update")
                .assertFailure()
                .resultChecker(FAIL_UPDATE_NO_ARGS);

        jar("ut")
            .assertFailure()
            .resultChecker(FAIL_TOO_MANY_MAIN_OPS);

        jar("-ut")
            .assertFailure()
            .resultChecker(FAIL_TOO_MANY_MAIN_OPS);

        if (!legacyOnly)
            jar("--update --list")
                .assertFailure()
                .resultChecker(FAIL_TOO_MANY_MAIN_OPS);
    }

    @Test
    public void updateReadFileWriteFile() throws IOException {
        Path path = Paths.get("updateReadWriteStdout.jar");  // for updating
        String jn = path.toString();

        for (String opts : new String[]{"uf " + jn, "-uf " + jn, "--update --file=" + jn}) {
            if (legacyOnly && opts.startsWith("--"))
                continue;

            createJar(path, RES1);
            jar(opts, RES2)
                .assertSuccess()
                .resultChecker(r -> {
                    ASSERT_CONTAINS_RES1.accept(Files.newInputStream(path));
                    ASSERT_CONTAINS_RES2.accept(Files.newInputStream(path));
                    ASSERT_CONTAINS_MAINFEST.accept(Files.newInputStream(path));
                });
        }
        FileUtils.deleteFileIfExistsWithRetry(path);
    }

    @Test
    public void updateReadStdinWriteStdout() throws IOException {
        Path path = Paths.get("updateReadStdinWriteStdout.jar");

        for (String opts : new String[]{"u", "-u", "--update"}) {
            if (legacyOnly && opts.startsWith("--"))
                continue;

            createJar(path, RES1);
            jarWithStdin(path.toFile(), opts, RES2)
                .assertSuccess()
                .resultChecker(r -> {
                    ASSERT_CONTAINS_RES1.accept(r.stdoutAsStream());
                    ASSERT_CONTAINS_RES2.accept(r.stdoutAsStream());
                    ASSERT_CONTAINS_MAINFEST.accept(r.stdoutAsStream());
                });
        }
        FileUtils.deleteFileIfExistsWithRetry(path);
    }

    @Test
    public void updateReadStdinWriteStdoutNoManifest() throws IOException {
        Path path = Paths.get("updateReadStdinWriteStdoutNoManifest.jar");

        for (String opts : new String[]{"uM", "-uM", "--update --no-manifest"} ){
            if (legacyOnly && opts.startsWith("--"))
                continue;

            createJar(path, RES1);
            jarWithStdin(path.toFile(), opts, RES2)
                .assertSuccess()
                .resultChecker(r -> {
                    ASSERT_CONTAINS_RES1.accept(r.stdoutAsStream());
                    ASSERT_CONTAINS_RES2.accept(r.stdoutAsStream());
                    ASSERT_DOES_NOT_CONTAIN_MAINFEST.accept(r.stdoutAsStream());
                });
        }
        FileUtils.deleteFileIfExistsWithRetry(path);
    }

    // List

    @Test
    public void listBadArgs() {
        jar("tx")
            .assertFailure()
            .resultChecker(FAIL_TOO_MANY_MAIN_OPS);

        jar("-tx")
            .assertFailure()
            .resultChecker(FAIL_TOO_MANY_MAIN_OPS);

        if (!legacyOnly)
            jar("--list --extract")
                .assertFailure()
                .resultChecker(FAIL_TOO_MANY_MAIN_OPS);
    }

    @Test
    public void listReadFromFileWriteToStdout() throws IOException {
        Path path = Paths.get("listReadFromFileWriteToStdout.jar");  // for listing
        createJar(path, RES1);
        String jn = path.toString();

        for (String opts : new String[]{"tf " + jn, "-tf " + jn, "--list --file " + jn}) {
            if (legacyOnly && opts.startsWith("--"))
                continue;

            jar(opts)
                .assertSuccess()
                .resultChecker(r ->
                    assertTrue(r.output.contains("META-INF/MANIFEST.MF") && r.output.contains(RES1),
                               "Failed, got [" + r.output + "]")
                );
        }
        FileUtils.deleteFileIfExistsWithRetry(path);
    }

    @Test
    public void listReadFromStdinWriteToStdout() throws IOException {
        Path path = Paths.get("listReadFromStdinWriteToStdout.jar");
        createJar(path, RES1);

        for (String opts : new String[]{"t", "-t", "--list"} ){
            if (legacyOnly && opts.startsWith("--"))
                continue;

            jarWithStdin(path.toFile(), opts)
                .assertSuccess()
                .resultChecker(r ->
                    assertTrue(r.output.contains("META-INF/MANIFEST.MF") && r.output.contains(RES1),
                               "Failed, got [" + r.output + "]")
                );
        }
        FileUtils.deleteFileIfExistsWithRetry(path);
    }

    // Extract

    @Test
    public void extractBadArgs() {
        jar("xi")
            .assertFailure()
            .resultChecker(FAIL_TOO_MANY_MAIN_OPS);

        jar("-xi")
            .assertFailure()
            .resultChecker(FAIL_TOO_MANY_MAIN_OPS);

        if (!legacyOnly) {
            jar("--extract --generate-index")
                .assertFailure()
                .resultChecker(new FailCheckerWithMessage(
                                   "option --generate-index requires an argument"));

            jar("--extract --generate-index=foo")
                .assertFailure()
                .resultChecker(FAIL_TOO_MANY_MAIN_OPS);
        }
    }

    @Test
    public void extractReadFromStdin() throws IOException {
        Path path = Paths.get("extract");
        Path jarPath = path.resolve("extractReadFromStdin.jar"); // for extracting
        createJar(jarPath, RES1);

        for (String opts : new String[]{"x" ,"-x", "--extract"}) {
            if (legacyOnly && opts.startsWith("--"))
                continue;

            jarWithStdinAndWorkingDir(jarPath.toFile(), path.toFile(), opts)
                .assertSuccess()
                .resultChecker(r ->
                    assertTrue(Files.exists(path.resolve(RES1)),
                               "Expected to find:" + path.resolve(RES1))
                );
            FileUtils.deleteFileIfExistsWithRetry(path.resolve(RES1));
        }
        FileUtils.deleteFileTreeWithRetry(path);
    }

    @Test
    public void extractReadFromFile() throws IOException {
        Path path = Paths.get("extract");
        String jn = "extractReadFromFile.jar";
        Path jarPath = path.resolve(jn);
        createJar(jarPath, RES1);

        for (String opts : new String[]{"xf "+jn ,"-xf "+jn, "--extract --file "+jn}) {
            if (legacyOnly && opts.startsWith("--"))
                continue;

            jarWithStdinAndWorkingDir(null, path.toFile(), opts)
                .assertSuccess()
                .resultChecker(r ->
                    assertTrue(Files.exists(path.resolve(RES1)),
                               "Expected to find:" + path.resolve(RES1))
                );
            FileUtils.deleteFileIfExistsWithRetry(path.resolve(RES1));
        }
        FileUtils.deleteFileTreeWithRetry(path);
    }

    // Basic help

    @Test
    public void helpBadOptionalArg() {
        if (legacyOnly)
            return;

        jar("--help:")
            .assertFailure();

        jar("--help:blah")
            .assertFailure();
    }

    @Test
    public void help() {
        if (legacyOnly)
            return;

        jar("-h")
            .assertSuccess()
            .resultChecker(r ->
                assertTrue(r.output.startsWith("Usage: jar [OPTION...] [ [--release VERSION] [-C dir] files]"),
                           "Failed, got [" + r.output + "]")
            );

        jar("--help")
            .assertSuccess()
            .resultChecker(r -> {
                assertTrue(r.output.startsWith("Usage: jar [OPTION...] [ [--release VERSION] [-C dir] files]"),
                           "Failed, got [" + r.output + "]");
                assertFalse(r.output.contains("--do-not-resolve-by-default"));
                assertFalse(r.output.contains("--warn-if-resolved"));
            });

        jar("--help:compat")
            .assertSuccess()
            .resultChecker(r ->
                assertTrue(r.output.startsWith("Compatibility Interface:"),
                           "Failed, got [" + r.output + "]")
            );

        jar("--help-extra")
            .assertSuccess()
            .resultChecker(r -> {
                assertTrue(r.output.startsWith("Usage: jar [OPTION...] [ [--release VERSION] [-C dir] files]"),
                           "Failed, got [" + r.output + "]");
                assertTrue(r.output.contains("--do-not-resolve-by-default"));
                assertTrue(r.output.contains("--warn-if-resolved"));
            });
    }

    // -- Infrastructure

    static boolean jarContains(JarInputStream jis, String entryName)
        throws IOException
    {
        JarEntry e;
        boolean found = false;
        while((e = jis.getNextJarEntry()) != null) {
            if (e.getName().equals(entryName))
                return true;
        }
        return false;
    }

    /* Creates a simple jar with entries of size 0, good enough for testing */
    static void createJar(Path path, String... entries) throws IOException {
        FileUtils.deleteFileIfExistsWithRetry(path);
        Path parent = path.getParent();
        if (parent != null)
            Files.createDirectories(parent);
        try (OutputStream out = Files.newOutputStream(path);
             JarOutputStream jos = new JarOutputStream(out)) {
            JarEntry je = new JarEntry("META-INF/MANIFEST.MF");
            jos.putNextEntry(je);
            jos.closeEntry();

            for (String entry : entries) {
                je = new JarEntry(entry);
                jos.putNextEntry(je);
                jos.closeEntry();
            }
        }
    }

    static class FailCheckerWithMessage implements Consumer<Result> {
        final String[] messages;
        FailCheckerWithMessage(String... m) {
            messages = m;
        }
        @Override
        public void accept(Result r) {
            //out.printf("%s%n", r.output);
            boolean found = false;
            for (String m : messages) {
                if (r.output.contains(m)) {
                    found = true;
                    break;
                }
            }
            assertTrue(found,
                       "Excepted out to contain one of: " + Arrays.asList(messages)
                           + " but got: " + r.output);
        }
    }

    static Result jar(String... args) {
        return jarWithStdinAndWorkingDir(null, null, args);
    }

    static Result jarWithStdin(File stdinSource, String... args) {
        return jarWithStdinAndWorkingDir(stdinSource, null, args);
    }

    static Result jarWithStdinAndWorkingDir(File stdinFrom,
                                            File workingDir,
                                            String... args) {
        String jar = getJDKTool("jar");
        List<String> commands = new ArrayList<>();
        commands.add(jar);
        if (!TOOL_VM_OPTIONS.isEmpty()) {
            commands.addAll(Arrays.asList(TOOL_VM_OPTIONS.split("\\s+", -1)));
        }
        Stream.of(args).map(s -> s.split(" "))
                       .flatMap(Arrays::stream)
                       .forEach(x -> commands.add(x));
        ProcessBuilder p = new ProcessBuilder(commands);
        if (stdinFrom != null)
            p.redirectInput(stdinFrom);
        if (workingDir != null)
            p.directory(workingDir);
        return run(p);
    }

    static Result run(ProcessBuilder pb) {
        Process p;
        byte[] stdout, stderr;
        out.printf("Running: %s%n", pb.command());
        try {
            p = pb.start();
        } catch (IOException e) {
            throw new RuntimeException(
                    format("Couldn't start process '%s'", pb.command()), e);
        }

        String output;
        try {
            stdout = readAllBytes(p.getInputStream());
            stderr = readAllBytes(p.getErrorStream());

            output = toString(stdout, stderr);
        } catch (IOException e) {
            throw new RuntimeException(
                    format("Couldn't read process output '%s'", pb.command()), e);
        }

        try {
            p.waitFor();
        } catch (InterruptedException e) {
            throw new RuntimeException(
                    format("Process hasn't finished '%s'", pb.command()), e);
        }
        return new Result(p.exitValue(), stdout, stderr, output);
    }

    static final Path JAVA_HOME = Paths.get(System.getProperty("java.home"));

    static String getJDKTool(String name) {
        try {
            return JDKToolFinder.getJDKTool(name);
        } catch (Exception x) {
            Path j = JAVA_HOME.resolve("bin").resolve(name);
            if (Files.exists(j))
                return j.toString();
            j = JAVA_HOME.resolve("..").resolve("bin").resolve(name);
            if (Files.exists(j))
                return j.toString();
            throw new RuntimeException(x);
        }
    }

    static String toString(byte[] ba1, byte[] ba2) {
        return (new String(ba1, UTF_8)).concat(new String(ba2, UTF_8));
    }

    static class Result {
        final int exitValue;
        final byte[] stdout;
        final byte[] stderr;
        final String output;

        private Result(int exitValue, byte[] stdout, byte[] stderr, String output) {
            this.exitValue = exitValue;
            this.stdout = stdout;
            this.stderr = stderr;
            this.output = output;
        }

        InputStream stdoutAsStream() { return new ByteArrayInputStream(stdout); }

        Result assertSuccess() { assertTrue(exitValue == 0, output); return this; }
        Result assertFailure() { assertTrue(exitValue != 0, output); return this; }

        Result resultChecker(IOConsumer<Result> r) {
            try {  r.accept(this); return this; }
            catch (IOException x) { throw new UncheckedIOException(x); }
        }

        Result resultChecker(FailCheckerWithMessage c) { c.accept(this); return this; }
    }

    interface IOConsumer<T> { void accept(T t) throws IOException ;  }

    // readAllBytes implementation so the test can be run pre 1.9 ( legacyOnly )
    static byte[] readAllBytes(InputStream is) throws IOException {
        byte[] buf = new byte[8192];
        int capacity = buf.length;
        int nread = 0;
        int n;
        for (;;) {
            // read to EOF which may read more or less than initial buffer size
            while ((n = is.read(buf, nread, capacity - nread)) > 0)
                nread += n;

            // if the last call to read returned -1, then we're done
            if (n < 0)
                break;

            // need to allocate a larger buffer
            capacity = capacity << 1;

            buf = Arrays.copyOf(buf, capacity);
        }
        return (capacity == nread) ? buf : Arrays.copyOf(buf, nread);
    }

    // Standalone entry point for running with, possibly older, JDKs.
    public static void main(String[] args) throws Throwable {
        boolean legacyOnly = false;
        if (args.length != 0 && args[0].equals("legacyOnly"))
            legacyOnly = true;

        CLICompatibility test = new CLICompatibility(legacyOnly);
        for (Method m : CLICompatibility.class.getDeclaredMethods()) {
            if (m.getAnnotation(Test.class) != null) {
                System.out.println("Invoking " + m.getName());
                m.invoke(test);
            }
        }
    }
    CLICompatibility(boolean legacyOnly) { this.legacyOnly = legacyOnly; }
    CLICompatibility() { this.legacyOnly = false; }
}