test/jdk/com/sun/jdi/lib/jdb/JdbTest.java
author dtitov
Tue, 09 Oct 2018 19:11:09 -0700
changeset 52068 218b5b64f102
parent 51836 d62ebdfd8f18
child 52291 fa61165a3f2b
permissions -rw-r--r--
8193879: Java debugger hangs on method invocation Reviewed-by: sspitsyn, amenkov, gadams

/*
 * Copyright (c) 2018, 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.
 */

package lib.jdb;

import jdk.test.lib.Utils;
import jdk.test.lib.process.OutputAnalyzer;
import jdk.test.lib.process.ProcessTools;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

public abstract class JdbTest {

    public static class LaunchOptions {
        public final String debuggeeClass;
        public final List<String> debuggeeOptions = new LinkedList<>();
        public String sourceFilename;

        public LaunchOptions(String debuggeeClass) {
            this.debuggeeClass = debuggeeClass;
        }
        public LaunchOptions addDebuggeeOption(String option) {
            debuggeeOptions.add(option);
            return this;
        }
        public LaunchOptions addDebuggeeOptions(String[] options) {
            debuggeeOptions.addAll(Arrays.asList(options));
            return this;
        }
        public LaunchOptions setSourceFilename(String name) {
            sourceFilename = name;
            return this;
        }
    }

    public JdbTest(LaunchOptions launchOptions) {
        this.launchOptions = launchOptions;
    }
    public JdbTest(String debuggeeClass) {
        this(new LaunchOptions(debuggeeClass));
    }

    // sourceFilename is used by setBreakpoints and redefineClass
    public JdbTest(String debuggeeClass, String sourceFilename) {
        this(new LaunchOptions(debuggeeClass).setSourceFilename(sourceFilename));
    }

    protected Jdb jdb;
    protected Process debuggee;
    private final List<String> debuggeeOutput = new LinkedList<>();
    private final LaunchOptions launchOptions;

    // returns the whole jdb output as a string
    public String getJdbOutput() {
        return jdb == null ? "" : jdb.getJdbOutput();
    }

    // returns the whole debuggee output as a string
    public String getDebuggeeOutput() {
        return debuggeeOutput.stream().collect(Collectors.joining(lineSeparator));
    }

    public void run() {
        try {
            setup();
            runCases();
        } catch (Throwable e) {
            jdb.log("=======================================");
            jdb.log("Exception thrown during test execution: " + e.getMessage());
            jdb.log("=======================================");
            throw e;
        } finally {
            shutdown();
        }
    }

    protected void setup() {
        /* run debuggee as:
            java -agentlib:jdwp=transport=dt_socket,address=0,server=n,suspend=y <debuggeeClass>
        it reports something like : Listening for transport dt_socket at address: 60810
        after that connect jdb by:
            jdb -connect com.sun.jdi.SocketAttach:port=60810
        */
        // launch debuggee
        List<String> debuggeeArgs = new LinkedList<>();
        // specify address=0 to automatically select free port
        debuggeeArgs.add("-agentlib:jdwp=transport=dt_socket,address=0,server=y,suspend=y");
        debuggeeArgs.addAll(launchOptions.debuggeeOptions);
        debuggeeArgs.add(launchOptions.debuggeeClass);
        ProcessBuilder pbDebuggee = ProcessTools.createJavaProcessBuilder(true, debuggeeArgs.toArray(new String[0]));

        // debuggeeListen[0] - transport, debuggeeListen[1] - address
        String[] debuggeeListen = new String[2];
        Pattern listenRegexp = Pattern.compile("Listening for transport \\b(.+)\\b at address: \\b(\\d+)\\b");
        try {
            debuggee = ProcessTools.startProcess("debuggee", pbDebuggee,
                    s -> debuggeeOutput.add(s),  // output consumer
                    s -> {  // warm-up predicate
                        Matcher m = listenRegexp.matcher(s);
                        if (!m.matches()) {
                            return false;
                        }
                        debuggeeListen[0] = m.group(1);
                        debuggeeListen[1] = m.group(2);
                        return true;
                    },
                    30, TimeUnit.SECONDS);
        } catch (IOException | InterruptedException | TimeoutException ex) {
            throw new RuntimeException("failed to launch debuggee", ex);
        }

        // launch jdb
        try {
            jdb = new Jdb("-connect", "com.sun.jdi.SocketAttach:port=" + debuggeeListen[1]);
        } catch (Throwable ex) {
            // terminate debuggee if something went wrong
            debuggee.destroy();
            throw ex;
        }
        // wait while jdb is initialized
        jdb.waitForPrompt(1, false);
    }

    protected abstract void runCases();

    protected void shutdown() {
        if (jdb != null) {
            jdb.shutdown();
        }
        // shutdown debuggee
        if (debuggee != null && debuggee.isAlive()) {
            try {
                debuggee.waitFor(Utils.adjustTimeout(10), TimeUnit.SECONDS);
            } catch (InterruptedException e) {
                // ignore
            } finally {
                if (debuggee.isAlive()) {
                    debuggee.destroy();
                }
            }
        }
    }

    protected static final String lineSeparator = System.getProperty("line.separator");


    // Parses the specified source file for "@{id} breakpoint" tags and returns
    // list of the line numbers containing the tag.
    // Example:
    //   System.out.println("BP is here");  // @1 breakpoint
    public static List<Integer> parseBreakpoints(String filePath, int id) {
        final String pattern = "@" + id + " breakpoint";
        int lineNum = 1;
        List<Integer> result = new LinkedList<>();
        try {
            for (String line: Files.readAllLines(Paths.get(filePath))) {
                if (line.contains(pattern)) {
                    result.add(lineNum);
                }
                lineNum++;
            }
        } catch (IOException ex) {
            throw new RuntimeException("failed to parse " + filePath, ex);
        }
        return result;
    }

    // sets breakpoints to the lines parsed by {@code parseBreakpoints}
    // returns number of the breakpoints set.
    public static int setBreakpoints(Jdb jdb, String debuggeeClass, String sourcePath, int id) {
        List<Integer> bps = parseBreakpoints(sourcePath, id);
        for (int bp : bps) {
            String reply = jdb.command(JdbCommand.stopAt(debuggeeClass, bp)).stream()
                    .collect(Collectors.joining("\n"));
            if (reply.contains("Unable to set")) {
                throw new RuntimeException("jdb failed to set breakpoint at " + debuggeeClass + ":" + bp);
            }

        }
        return bps.size();
    }

    // sets breakpoints to the lines parsed by {@code parseBreakpoints}
    // from the file from test source directory.
    // returns number of the breakpoints set.
    protected int setBreakpointsFromTestSource(String debuggeeFileName, int id) {
        return setBreakpoints(jdb, launchOptions.debuggeeClass,
                              getTestSourcePath(debuggeeFileName), id);
    }

    // sets breakpoints in the class {@code launchOptions.debuggeeClass}
    // to the lines parsed by {@code parseBreakpoints}
    // from the file from test source directory specified by {@code launchOptions.sourceFilename}.
    // returns number of the breakpoints set.
    protected int setBreakpoints(int id) {
        verifySourceFilename();
        return setBreakpointsFromTestSource(launchOptions.sourceFilename, id);
    }

    // transforms class with the specified id (see {@code ClassTransformer})
    // and executes "redefine" jdb command for {@code launchOptions.debuggeeClass}.
    // returns reply for the command.
    protected List<String> redefineClass(int id, String... compilerOptions) {
        verifySourceFilename();
        String transformedClassFile = ClassTransformer.fromTestSource(launchOptions.sourceFilename)
                .transform(id, launchOptions.debuggeeClass, compilerOptions);
        return jdb.command(JdbCommand.redefine(launchOptions.debuggeeClass, transformedClassFile));
    }

    // gets full test source path for the given test filename
    public static String getTestSourcePath(String fileName) {
        return Paths.get(System.getProperty("test.src")).resolve(fileName).toString();
    }

    // verifies that sourceFilename is specified in ctor
    private void verifySourceFilename() {
        if (launchOptions.sourceFilename == null) {
            throw new RuntimeException("launchOptions.sourceFilename must be specified.");
        }
    }

    protected OutputAnalyzer execCommand(JdbCommand cmd) {
        List<String> reply = jdb.command(cmd);
        return new OutputAnalyzer(reply.stream().collect(Collectors.joining(lineSeparator)));
    }

    // helpers for "eval" jdb command.
    // executes "eval <expr>" and verifies output contains the specified text
    protected void evalShouldContain(String expr, String expectedString) {
        execCommand(JdbCommand.eval(expr))
                .shouldContain(expectedString);
    }
    // executes "eval <expr>" and verifies output does not contain the specified text
    protected void evalShouldNotContain(String expr, String unexpectedString) {
        execCommand(JdbCommand.eval(expr))
                .shouldNotContain(unexpectedString);
    }
}