/*
* The MIT License
*
* Copyright (c) 2014 Red Hat, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.github.olivergondza.dumpling.factory;
import static com.github.olivergondza.dumpling.Util.currentProcessOut;
import static com.github.olivergondza.dumpling.Util.only;
import static com.github.olivergondza.dumpling.Util.pause;
import static com.github.olivergondza.dumpling.Util.processTerminatedPrematurely;
import static com.github.olivergondza.dumpling.model.ProcessThread.nameIs;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertTrue;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import javax.annotation.Nonnull;
import com.github.olivergondza.dumpling.model.ModelObject;
import org.hamcrest.Matchers;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.MethodRule;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.Statement;
import com.github.olivergondza.dumpling.Util;
import com.github.olivergondza.dumpling.model.StackTrace;
import com.github.olivergondza.dumpling.model.ThreadLock;
import com.github.olivergondza.dumpling.model.ThreadStatus;
import com.github.olivergondza.dumpling.model.dump.ThreadDumpRuntime;
import com.github.olivergondza.dumpling.model.dump.ThreadDumpThread;
/**
* Make sure threaddump generated by vendor of current JVM can be read by dumpling.
*
* Rule will start groovy script from <tt>src/test/resources/com/github/olivergondza/dumpling/factory/ThreadDumpFactoryVendorTest/METHOD_NAME.groovy</tt>
* in separated process. SUT is picked up by <tt>sut.runtime()</tt> or
* <tt>sut.thread("name")</tt>, the method will wait for process to be ready.
*
* This belongs to core module though it is convenient to use Dumpling CLI as groovy interpreter.
*
* @author ogondza
*/
public class ThreadDumpFactoryVendorTest {
public @Rule Runner sut = new Runner();
@Test
public void busyWaiting() {
ThreadDumpThread main = sut.thread("main");
assertThat(main.getStatus(), equalTo(ThreadStatus.RUNNABLE));
assertThat(main.getAcquiredLocks(), Matchers.<ThreadLock>empty());
assertThat(main.getWaitingToLock(), nullValue());
}
@Test
public void waiting() {
ThreadDumpThread main = sut.thread("main");
assertThat(main.getStatus(), equalTo(ThreadStatus.IN_OBJECT_WAIT));
assertThat(main.getAcquiredLocks(), Matchers.<ThreadLock>empty());
assertThat(main.getWaitingToLock(), nullValue());
assertThat(main.getWaitingOnLock().getClassName(), equalTo("java.lang.Object"));
}
@Test
public void waitingTimed() {
ThreadDumpThread main = sut.thread("main");
assertThat(main.getStatus(), equalTo(ThreadStatus.IN_OBJECT_WAIT_TIMED));
assertThat(main.getAcquiredLocks(), Matchers.<ThreadLock>empty());
assertThat(main.getWaitingToLock(), nullValue());
assertThat(main.getWaitingOnLock().getClassName(), equalTo("java.lang.Object"));
}
@Test
public void sleeping() {
ThreadDumpThread main = sut.thread("main");
assertThat(main.getStatus(), equalTo(ThreadStatus.SLEEPING));
assertThat(main.getAcquiredLocks(), Matchers.<ThreadLock>empty());
assertThat(main.getWaitingToLock(), nullValue());
assertThat(
main.getStackTrace().getElement(0),
equalTo(StackTrace.nativeElement("java.lang.Thread", "sleep"))
);
}
@Test
public void monitorDeadlock() {
ThreadDumpThread main = sut.thread("main");
ThreadDumpThread other = sut.thread("other");
assertThat(main.getStatus(), equalTo(ThreadStatus.BLOCKED));
assertThat(other.getStatus(), equalTo(ThreadStatus.BLOCKED));
assertThat(main.getWaitingToLock(), equalTo(only(other.getAcquiredMonitors())));
assertThat(other.getWaitingToLock(), equalTo(only(main.getAcquiredMonitors())));
assertThat(other.getAcquiredSynchronizers(), Matchers.<ThreadLock>empty());
assertThat(main.getAcquiredSynchronizers(), Matchers.<ThreadLock>empty());
assertThat(main.getBlockingThread(), equalTo(other));
assertThat(other.getBlockingThread(), equalTo(main));
assertThat(main.getBlockedThreads().onlyThread(), equalTo(other));
assertThat(other.getBlockedThreads().onlyThread(), equalTo(main));
}
@Test
public void synchronizerDeadlock() {
ThreadDumpThread main = sut.thread("main");
ThreadDumpThread other = sut.thread("other");
assertThat(main.getStatus(), equalTo(ThreadStatus.PARKED));
assertThat(other.getStatus(), equalTo(ThreadStatus.PARKED));
assertThat(main.getWaitingOnLock(), equalTo(only(other.getAcquiredSynchronizers())));
assertThat(other.getWaitingOnLock(), equalTo(only(main.getAcquiredSynchronizers())));
assertThat(other.getAcquiredMonitors(), Matchers.<ThreadLock>empty());
assertThat(main.getAcquiredMonitors(), Matchers.<ThreadLock>empty());
}
@Test
public void reacquireMonitorAfterWait() {
ThreadDumpThread reacquiring = sut.thread("reacquiring");
ThreadDumpThread main = sut.thread("main");
assertThat(main.getStatus(), equalTo(ThreadStatus.SLEEPING));
assertThat(reacquiring.getStatus(), equalTo(ThreadStatus.BLOCKED));
assertThat(only(main.getAcquiredLocks()), equalTo(reacquiring.getWaitingToLock()));
assertThat(reacquiring.getAcquiredLocks().size(), equalTo(2));
assertThat(
reacquiring.getStackTrace().getElement(0),
equalTo(StackTrace.nativeElement("java.lang.Object", "wait"))
);
}
@Test
public void ignoreWaitLock() {
ThreadDumpThread main = sut.thread("main");
assertThat(main.getStatus(), equalTo(ThreadStatus.IN_OBJECT_WAIT));
assertThat(main.getWaitingToLock(), nullValue());
assertThat(only(main.getAcquiredLocks()).getClassName(), equalTo("java.lang.Integer"));
assertTrue(main.getBlockedThreads().isEmpty());
assertThat(main.getBlockingThread(), nullValue());
assertThat(main.getWaitingOnLock().getClassName(), equalTo("java.lang.Double"));
}
@Test
public void parked() {
ThreadDumpThread main = sut.thread("main");
assertThat(main.getStatus(), equalTo(ThreadStatus.PARKED));
assertThat(main.getAcquiredLocks(), Matchers.<ThreadLock>empty());
assertThat(main.getWaitingToLock(), nullValue());
assertThat(main.getWaitingOnLock(), nullValue());
assertThat(
main.getStackTrace().getElement(0).getMethodName(),
equalTo("park")
);
}
@Test
public void parkedTimed() {
ThreadDumpThread main = sut.thread("main");
assertThat(main.getStatus(), equalTo(ThreadStatus.PARKED_TIMED));
assertThat(main.getAcquiredLocks(), Matchers.<ThreadLock>empty());
assertThat(main.getWaitingToLock(), nullValue());
assertThat(main.getWaitingOnLock(), nullValue());
assertThat(
main.getStackTrace().getElement(0).getMethodName(),
equalTo("park")
);
}
@Test
public void parkedWithBlocker() {
ThreadDumpThread main = sut.thread("main");
assertThat(main.getStatus(), equalTo(ThreadStatus.PARKED));
assertThat(main.getAcquiredLocks(), Matchers.<ThreadLock>empty());
assertThat(main.getWaitingOnLock().getClassName(), equalTo("java.lang.Object"));
assertThat(
main.getStackTrace().getElement(0).getMethodName(),
equalTo("park")
);
}
@Test
public void parkedTimedWithBlocker() {
ThreadDumpThread main = sut.thread("main");
assertThat(main.getStatus(), equalTo(ThreadStatus.PARKED_TIMED));
assertThat(main.getAcquiredLocks(), Matchers.<ThreadLock>empty());
assertThat(main.getWaitingOnLock().getClassName(), equalTo("java.lang.Object"));
assertThat(
main.getStackTrace().getElement(0).getMethodName(),
equalTo("park")
);
}
@Test
public void threadNameWithQuotes() {
ThreadDumpThread t = sut.thread("a\"thread\"");
assertThat(t.getName(), equalTo("a\"thread\""));
}
@Test
public void threadNameWithLinebreak() {
String name = "thread" + System.getProperty("line.separator") + "name";
ThreadDumpThread t = sut.thread(name);
assertThat(t.getName(), equalTo(name));
}
@Test
public void multipleMonitorsOnSingleStackFrame() {
ThreadDumpThread main = sut.thread("main");
assertThat(main.getAcquiredLocks(), equalTo(main.getAcquiredMonitors()));
// Stack depth is not exposed, checking just the order
ArrayList<ThreadLock> lockList = new ArrayList<ThreadLock>(main.getAcquiredLocks());
assertThat(lockList.get(0).getClassName(), equalTo("java.lang.Double"));
assertThat(lockList.get(1).getClassName(), equalTo("java.lang.Integer"));
assertThat(lockList.get(2).getClassName(), equalTo("java.lang.Object"));
}
private static final class Runner implements MethodRule {
public static final ThreadDumpFactory THREAD_DUMP_FACTORY = new ThreadDumpFactory().failOnErrors(true);
private final PidRuntimeFactory prf = new PidRuntimeFactory() {
@Override
protected ThreadDumpRuntime createRuntime(Process process) {
threaddump = Util.asString(process.getInputStream());
return THREAD_DUMP_FACTORY.fromString(threaddump);
}
};
private Process process;
private String processLine;
private ThreadDumpRuntime runtime = null;
private File pidFile = null;
private File scriptFile = null;
private String threaddump = null;
@Override
public Statement apply(final Statement base, final FrameworkMethod method, Object target) {
try {
String init = Util.asString(Util.resource(
ThreadDumpFactoryVendorTest.class, "_init.groovy"
));
String script = Util.asString(Util.resource(
ThreadDumpFactoryVendorTest.class, method.getName() + ".groovy"
));
run(init + script);
} catch (Exception ex) {
throw new AssertionError(ex);
}
return new Statement() {
@Override
public void evaluate() throws Throwable {
try {
base.evaluate();
} catch (Throwable ex) {
if (runtime != null) {
System.err.printf("%s has failed with %s: %s%n", method.getName(), ex.getClass(), ex.getMessage());
System.err.println("Original runtime:");
System.err.println(threaddump);
System.err.println("Parsed runtime:");
runtime.toString(System.err, ModelObject.Mode.MACHINE);
}
throw ex;
} finally {
if (pidFile != null && pidFile.exists()) pidFile.delete();
if (scriptFile != null && scriptFile.exists()) scriptFile.delete();
if (process != null) process.destroy();
}
}
};
}
private void run(String script) throws IOException {
pidFile = File.createTempFile("dumpling", getClass().getName() + ".pid");
pidFile.deleteOnExit();
scriptFile = File.createTempFile("dumpling", getClass().getName() + ".script");
scriptFile.deleteOnExit();
PrintWriter writer = new PrintWriter(scriptFile);
try {
writer.print(script);
} finally {
writer.close();
}
ProcessBuilder pb = Util.processBuilder().command(
System.getProperty("java.home") + "/bin/java",
"-cp", System.getProperty("surefire.real.class.path"), // Inherit from surefire process
"com.github.olivergondza.dumpling.cli.Main",
"groovy",
"--script", scriptFile.getAbsolutePath(),
pidFile.getAbsolutePath()
);
processLine = pb.command().toString();
process = pb.start();
}
public ThreadDumpThread thread(@Nonnull String name) {
return runtime().getThreads().where(nameIs(name)).onlyThread();
}
public ThreadDumpRuntime runtime() {
if (runtime != null) return runtime;
try {
int exit = process.exitValue();
throw processTerminatedPrematurely(process, exit, processLine);
} catch (IllegalThreadStateException ex) {
// Still running as expected
try {
return runtime = waitForInitialized();
} catch (IOException e) {
String message = String.format(
"Unable to get Runtime from %s, Exit code: %d%nSTDOUT: %s%nSTDERR: %s",
processLine,
getExitIfDone(),
currentProcessOut(process.getInputStream()),
currentProcessOut(process.getErrorStream())
);
throw new Error(message, e);
} catch (InterruptedException e) {
throw new Error("Interrupted: " + processLine, e);
}
}
}
private Integer getExitIfDone() {
try {
return process.exitValue();
} catch (IllegalThreadStateException ex) {
return null;
}
}
private ThreadDumpRuntime waitForInitialized() throws IOException, InterruptedException {
int pid = getPid();
ThreadDumpRuntime runtime = prf.fromProcess(pid);
for (int i = 0; i < 10; i++) {
pause(500);
ThreadDumpThread main = runtime.getThreads().where(nameIs("main")).onlyThread();
for (StackTraceElement elem: main.getStackTrace().getElements()) {
if ("dumpling-script".equals(elem.getClassName()) && "run".equals(elem.getMethodName())) {
return runtime;
}
}
runtime = prf.fromProcess(pid);
}
throw new IOException("Process under test not initialized in time: " + runtime.getThreads());
}
private int getPid() throws IOException {
if (pidFile == null) throw new IOException("pidfile not initialized");
IOException ex = null;
// Wait for pidfile to be written
for (int i = 0; i < 5; i++) {
if (pidFile.exists()) {
try {
return Integer.parseInt(Util.fileContents(pidFile).trim());
} catch (NumberFormatException e) {
ex = new IOException("unable to read PID from " + pidFile, e);
}
}
pause(1000);
}
if (ex != null) throw ex;
throw new IOException("SUT process not running. pidfile: " + pidFile);
}
}
}