core/src/main/java/com/github/olivergondza/dumpling/model/ThreadLock.java
public class ThreadLock extends ModelObject {

    protected final @Nonnull String className;
    private final long id;

    public static @Nonnull ThreadLock fromInstance(@Nonnull Object instance) {
        return new ThreadLock(
                instance.getClass().getCanonicalName(),
                System.identityHashCode(instance)
        );
    }

    public ThreadLock(@Nonnull String className, long id) {
        this.className = className;
        this.id = id;
    }

    public @Nonnull String getClassName() {
        return className;
    }

    public long getId() {
        return id;
    }

    @Override
    public boolean equals(Object lhs) {
        if (lhs == null) return false;
        if (!(lhs instanceof ThreadLock)) return false;

        ThreadLock other = (ThreadLock) lhs;
        return id == other.id;
    }

    @Override
    public int hashCode() {
        return 7 + 31 * new Long(id).hashCode();
    }

    @Override
    public void toString(PrintStream stream, Mode mode) {
        String format = mode.isHuman() ? "<0x%x> (a %s)" : "<0x%016x> (a %s)";
        stream.format(format, id, className);

    }

    /**
     * Monitor with stack trace position.
     *
     * @author ogondza
     */
    public static final class Monitor {

        private final @Nonnegative int depth;
        private final @Nonnull ThreadLock lock;

        public Monitor(@Nonnull ThreadLock lock, @Nonnegative int depth) {
            this.depth = depth;
            this.lock = lock;
        }

        public @Nonnegative int getDepth() {
            return depth;
        }

        public @Nonnull ThreadLock getLock() {
            return lock;
        }

        @Override
        public boolean equals(Object lhs) {
            if (lhs == null) return false;
            if (!lhs.getClass().equals(this.getClass())) return false;

            Monitor other = (Monitor) lhs;
            return depth == other.depth && lock.equals(other.lock);
        }

        @Override
        public int hashCode() {
            return lock.hashCode() + depth;
        }

        @Override
        public String toString() {
            return lock.toString();
        }
    }
}
test-utils/src/main/java/com/github/olivergondza/dumpling/Util.java
public static void pause(int time) {
    try {
        Thread.sleep(time);
    } catch (InterruptedException ex) {
    }
}
test-utils/src/main/java/com/github/olivergondza/dumpling/Util.java
public static String currentProcessOut(InputStream os) {
    final StringBuilder out = new StringBuilder();
    try {
        int length = os.available();
        byte[] buffer = new byte[length];
        int read = os.read(buffer, 0, length);
        if (read != -1) { // No out
            out.append(new String(buffer));
        }
    } catch (IOException ex) {
        throw new Error(ex);
    }

    return out.toString();
}
core/src/main/java/com/github/olivergondza/dumpling/model/ProcessThread.java
public static @Nonnull Predicate nameIs(final @Nonnull String name) {
    return new Predicate() {
        @Override
        public boolean isValid(@Nonnull ProcessThread<?, ?, ?> thread) {
            return thread.getName().equals(name);
        }
    };
}
core/src/main/java/com/github/olivergondza/dumpling/model/StackTrace.java
public class StackTrace extends ModelObject {

    public static StackTraceElement element(String declaringClass, String methodName, String fileName, int lineNumber) {
        return new StackTraceElement(declaringClass, methodName, fileName, lineNumber);
    }

    /**
     * Get {@link StackTraceElement} its source line number is unknown.
     */
    public static StackTraceElement element(String declaringClass, String methodName, String fileName) {
        return new StackTraceElement(declaringClass, methodName, fileName, -1);
    }

    /**
     * Get {@link StackTraceElement} its source (file name and line number) is unknown.
     */
    public static StackTraceElement element(String declaringClass, String methodName) {
        return new StackTraceElement(declaringClass, methodName, null, -1);
    }

    /**
     * Get {@link StackTraceElement} for native method.
     */
    public static StackTraceElement nativeElement(String declaringClass, String methodName) {
        return new StackTraceElement(declaringClass, methodName, null, -2);
    }

    /**
     * Get {@link StackTraceElement} for native method.
     *
     * Some native method {@link StackTraceElement}s have filename, even though it is not shown.
     */
    public static StackTraceElement nativeElement(String declaringClass, String methodName, String fileName) {
        return new StackTraceElement(declaringClass, methodName, fileName, -2);
    }

    private @Nonnull StackTraceElement[] elements;

    public StackTrace(@Nonnull StackTraceElement... elements) {
        this.elements = elements.clone(); // Shallow copy is ok here as StackTraceElement is immutable
    }

    public StackTrace(@Nonnull List<StackTraceElement> elements) {
        this.elements = elements.toArray(new StackTraceElement[elements.size()]);
    }

    public int size() {
        return elements.length;
    }

    /**
     * Get element of given stack depth.
     *
     * @return Stack element of null if not present.
     */
    public @CheckForNull StackTraceElement getElement(@Nonnegative int depth) {
        if (depth < 0) throw new ArrayIndexOutOfBoundsException(depth);

        return elements.length > depth
                ? elements[depth]
                : null
        ;
    }

    /**
     * Get innermost stack frame or null when there is no trace attached.
     */
    public @CheckForNull StackTraceElement getHead() {
        return getElement(0);
    }

    public @CheckForNull StackTraceElement head() {
        return getElement(0);
    }

    /**
     * Get all the stack trace elements.
     */
    public @Nonnull List<StackTraceElement> getElements() {
        return Arrays.asList(elements);
    }

    @Override
    public void toString(PrintStream stream, Mode mode) {
        for (StackTraceElement e: elements) {
            stream.println();
            stream.append("\tat ").append(e.toString());
        }

        stream.println();
    }

    @Override
    public int hashCode() {
        return 31 * Arrays.hashCode(elements);
    }

    @Override
    public boolean equals(Object rhs) {
        if (this == rhs) return true;
        if (rhs == null) return false;
        if (getClass() != rhs.getClass()) return false;

        StackTrace other = (StackTrace) rhs;
        if (!Arrays.equals(elements, other.elements)) return false;
        return true;
    }
}
core/src/main/java/com/github/olivergondza/dumpling/model/dump/ThreadDumpRuntime.java
public final class ThreadDumpRuntime extends ProcessRuntime<ThreadDumpRuntime, ThreadDumpThreadSet, ThreadDumpThread> {

    /**
     * Threaddump header, either empty or terminated wit blank line so it can be prepended to the ThreadSet.
     */
    private final @Nonnull List<String> header;

    public ThreadDumpRuntime(@Nonnull Set<ThreadDumpThread.Builder> builders, @Nonnull List<String> header) {
        super(builders);
        this.header = new ArrayList<String>(header);
    }

    @Override
    protected ThreadDumpThreadSet createSet(Set<ThreadDumpThread> threads) {
        return new ThreadDumpThreadSet(this, threads);
    }

    @Override
    protected ThreadDumpThread createThread(Builder<?> builder) {
        return new ThreadDumpThread(this, (ThreadDumpThread.Builder) builder);
    }

    @Override
    public void toString(PrintStream stream, Mode mode) {
        if (!header.isEmpty()) {
            for (String line: header) {
                stream.println(line);
            }
            stream.println();
        }

        super.toString(stream, mode);
    }
}
test-utils/src/main/java/com/github/olivergondza/dumpling/Util.java
public static <T> T only(Iterable<T> set) {
    Iterator<T> it = set.iterator();
    T elem = it.next();
    if (elem == null || it.hasNext()) throw new AssertionError(
            "One set element expected: " + set
    );

    return elem;
}
core/src/main/java/com/github/olivergondza/dumpling/model/dump/ThreadDumpThread.java
public final class ThreadDumpThread extends ProcessThread<ThreadDumpThread, ThreadDumpThreadSet, ThreadDumpRuntime> {

    /*package*/ ThreadDumpThread(@Nonnull ThreadDumpRuntime runtime, @Nonnull ThreadDumpThread.Builder builder) {
        super(runtime, builder);
    }

    public final static class Builder extends ProcessThread.Builder<Builder> {
    }
}
core/src/main/java/com/github/olivergondza/dumpling/model/ThreadStatus.java
public enum ThreadStatus {
    NEW                 ("NEW",                                 State.NEW),
    RUNNABLE            ("RUNNABLE",                            State.RUNNABLE),
    SLEEPING            ("TIMED_WAITING (sleeping)",            State.TIMED_WAITING),

    /**
     * Thread in {@link Object#wait()}.
     *
     * @see java.lang.Thread.State#WAITING
     */
    IN_OBJECT_WAIT      ("WAITING (on object monitor)",         State.WAITING),

    /**
     * Thread in {@link Object#wait(long)}.
     *
     * @see java.lang.Thread.State#TIMED_WAITING
     */
    IN_OBJECT_WAIT_TIMED("TIMED_WAITING (on object monitor)",   State.TIMED_WAITING),

    /**
     * Thread in {@link LockSupport#park()}.
     *
     * @see java.lang.Thread.State#WAITING
     */
    PARKED              ("WAITING (parking)",                   State.WAITING),

    /**
     * Thread in {@link LockSupport#parkNanos}.
     *
     * @see java.lang.Thread.State#TIMED_WAITING
     */
    PARKED_TIMED        ("TIMED_WAITING (parking)",             State.TIMED_WAITING),

    BLOCKED             ("BLOCKED (on object monitor)",         State.BLOCKED),
    TERMINATED          ("TERMINATED",                          State.TERMINATED),

    /**
     * Runtime factory was not able to determine thread state.
     *
     * This can happen for service threads in threaddump.
     */
    UNKNOWN             ("UNKNOWN",                             null);

    /**
     * Description used in thread dump.
     */
    private final @Nonnull String name;

    /**
     * Matching java.lang.Thread.State.
     */
    private final State state;

    ThreadStatus(@Nonnull String name, State state) {
        this.name = name;
        this.state = state;
    }

    public @Nonnull String getName() {
        return name;
    }

    public @CheckForNull State getState() {
        return state;
    }

    /**
     * Newly create thread
     *
     * @see java.lang.Thread.State#NEW
     */
    public boolean isNew() {
        return this == NEW;
    }

    /**
     * Thread that is runnable / running.
     *
     * @see java.lang.Thread.State#RUNNABLE
     */
    public boolean isRunnable() {
        return this == RUNNABLE;
    }

    /**
     * Thread in {@link Thread#sleep(long)} or {@link Thread#sleep(long, int)} waiting to be notified.
     *
     * @see java.lang.Thread.State#TIMED_WAITING
     */
    public boolean isSleeping() {
        return this == SLEEPING;
    }

    /**
     * Thread in {@link Object#wait()} or {@link Object#wait(long)}.
     *
     * @see java.lang.Thread.State#WAITING
     * @see java.lang.Thread.State#TIMED_WAITING
     */
    public boolean isWaiting() {
        return this == IN_OBJECT_WAIT || this == IN_OBJECT_WAIT_TIMED;
    }

    /**
     * Thread in {@link LockSupport#park()} or {@link LockSupport#parkNanos}.
     *
     * @see java.lang.Thread.State#WAITING
     * @see java.lang.Thread.State#TIMED_WAITING
     */
    public boolean isParked() {
        return this == PARKED || this == PARKED_TIMED;
    }

    /**
     * Thread (re-)entering a synchronization block.
     *
     * @see java.lang.Thread.State#BLOCKED
     */
    public boolean isBlocked() {
        return this == BLOCKED;
    }

    /**
     * Thread that terminated execution.
     *
     * @see java.lang.Thread.State#TERMINATED
     */
    public boolean isTerminated() {
        return this == TERMINATED;
    }

    public static @Nonnull ThreadStatus fromString(String title) {
        try {

            @SuppressWarnings("null")
            final @Nonnull ThreadStatus value = Enum.valueOf(ThreadStatus.class, title);
            return value;
        } catch (IllegalArgumentException ex) {

            for (ThreadStatus value: values()) {
                if (value.getName().equals(title)) return value;
            }

            throw ex;
        }
    }

    public static @Nonnull ThreadStatus fromState(@Nonnull Thread.State state, @CheckForNull StackTraceElement head) {
        switch (state) {
            case NEW: return NEW;
            case RUNNABLE: return RUNNABLE;
            case BLOCKED: return BLOCKED;
            case WAITING: return waitingState(false, head); // Not exact
            case TIMED_WAITING: return waitingState(true, head); // Not exact
            case TERMINATED: return TERMINATED;
            default: return UNKNOWN;
        }
    }

    private static @Nonnull ThreadStatus waitingState(boolean timed, @CheckForNull StackTraceElement head) {
        if (head == null) return ThreadStatus.UNKNOWN;
        if ("sleep".equals(head.getMethodName()) && "java.lang.Thread".equals(head.getClassName())) return SLEEPING;
        if ("wait".equals(head.getMethodName()) && "java.lang.Object".equals(head.getClassName())) {
            return timed ? IN_OBJECT_WAIT_TIMED : IN_OBJECT_WAIT;
        }

        if ("park".equals(head.getMethodName()) && UNSAFE.contains(head.getClassName())) {
            return timed ? PARKED_TIMED : PARKED;
        }

        throw new AssertionError("Unable to infer ThreadStatus from WAITING state in " + head);
    }

    public static final List<String> UNSAFE = Arrays.asList(
            "sun.misc.Unsafe", // hotspot JDK 8 and older
            "jdk.internal.misc.Unsafe" // hotspot JDK 9
    );
}
core/src/main/java/com/github/olivergondza/dumpling/model/ModelObject.java
public abstract class ModelObject {

    /**
     * Print object into stream.
     */
    public abstract void toString(@Nonnull PrintStream stream, @Nonnull Mode mode);

    /**
     * Print model object to string in specified mode.
     */
    public String toString(@Nonnull Mode mode) {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        toString(new PrintStream(baos), mode);
        return baos.toString();
    }

    /**
     * Default toString implementation for ModelObject.
     */
    @Override
    public String toString() {
        return toString(Mode.HUMAN);
    }

    /**
     * Print output specific for given mode.
     *
     * @author ogondza
     */
    public enum Mode {
        /**
         * Well-defined output format to be consumed by other programs.
         */
        MACHINE,
        /**
         * Pretty printed output easy to read for human.
         */
        HUMAN;

        public boolean isMachine() {
            return this == MACHINE;
        }

        public boolean isHuman() {
            return this == HUMAN;
        }
    }
}
test-utils/src/main/java/com/github/olivergondza/dumpling/Util.java
public static Error processTerminatedPrematurely(Process process, int exit, String line) {
    AssertionError error = new AssertionError(
            String.format(
                    "Process under test '%s' probably terminated prematurely. Exit code: %d%nSTDOUT: %s%nSTDERR: %s",
                    line,
                    exit,
                    Util.asString(process.getInputStream()), // Process exited - we are ok to blocking read the outs.
                    Util.asString(process.getErrorStream())
            )
    );
    return error;
}
test-utils/src/main/java/com/github/olivergondza/dumpling/Util.java
public class Util {

    public static final @Nonnull String NL = System.getProperty("line.separator", "\n");

    public static InputStream resource(Class<?> testClass, String resource) {
        InputStream res = testClass.getResourceAsStream(testClass.getSimpleName() + "/" + resource);
        if (res == null) throw new AssertionError("Resource does not exist: " + testClass.getSimpleName() + "/" + resource);
        return res;
    }

    public static InputStream resource(String resource) {
        if (!resource.startsWith("/")) {
            resource = "/" + resource;
        }
        InputStream res = Util.class.getResourceAsStream(resource);
        if (res == null) throw new AssertionError("Resource does not exist: " + resource);
        return res;
    }

    public static File asFile(InputStream is, File file) throws IOException {
        FileWriter fw = new FileWriter(file);
        try {
            byte[] buffer = new byte[1024];
            int length;
            while ((length = is.read(buffer)) != -1) {
                fw.append(new String(buffer, 0, length));
            }
        } finally {
            fw.close();
        }

        return file;
    }

    public static File asFile(InputStream is) {
        File file = null;
        try {
            file = File.createTempFile("dumpling", "streamFile");
            file.deleteOnExit();
            return asFile(is, file);
        } catch (IOException ex) {
            if (file != null) {
                file.delete();
            }
            throw new AssertionError(ex);
        }
    }

    public static String asString(InputStream is) {
        StringBuilder sb = new StringBuilder();
        try {
            byte[] buffer = new byte[1024];
            int length;
            while ((length = is.read(buffer)) != -1) {
                sb.append(new String(buffer, 0, length));
            }
            return sb.toString();
        } catch (IOException ex) {
            throw new AssertionError(ex);
        }
    }

    public static void forwardStream(InputStream in, PrintStream out) throws IOException {
        byte[] buffer = new byte[1024];
        int len;
        while ((len = in.read(buffer)) != -1) {
            out.write(buffer, 0, len);
        }
    }

    public static void pause(int time) {
        try {
            Thread.sleep(time);
        } catch (InterruptedException ex) {
        }
    }

    public static String formatTrace(String... frames) {
        StringBuilder sb = new StringBuilder();
        for (String frame: frames) {
            sb.append('\t').append(frame).append(NL);
        }

        return sb.toString();
    }

    public static String multiline(String... lines) {
        StringBuilder sb = new StringBuilder();
        for (String line: lines) {
            sb.append(line).append(NL);
        }

        return sb.toString();
    }

    public static int currentPid() {
        return Integer.parseInt(ManagementFactory.getRuntimeMXBean().getName().replaceAll("@.*", ""));
    }

    public static <T> T only(Iterable<T> set) {
        Iterator<T> it = set.iterator();
        T elem = it.next();
        if (elem == null || it.hasNext()) throw new AssertionError(
                "One set element expected: " + set
        );

        return elem;
    }

    public static String currentProcessOut(InputStream os) {
        final StringBuilder out = new StringBuilder();
        try {
            int length = os.available();
            byte[] buffer = new byte[length];
            int read = os.read(buffer, 0, length);
            if (read != -1) { // No out
                out.append(new String(buffer));
            }
        } catch (IOException ex) {
            throw new Error(ex);
        }

        return out.toString();
    }

    public static String fileContents(File file) throws IOException {
        Scanner scanner = null;
        try {
            scanner = new Scanner(file);
            return scanner.useDelimiter("\\A").next();
        } catch (NoSuchElementException ex) {
            return ""; // empty
        } finally {
            if (scanner != null) scanner.close();
        }
    }

    public static Error processTerminatedPrematurely(Process process, int exit, String line) {
        AssertionError error = new AssertionError(
                String.format(
                        "Process under test '%s' probably terminated prematurely. Exit code: %d%nSTDOUT: %s%nSTDERR: %s",
                        line,
                        exit,
                        Util.asString(process.getInputStream()), // Process exited - we are ok to blocking read the outs.
                        Util.asString(process.getErrorStream())
                )
        );
        return error;
    }

    public static ProcessBuilder processBuilder() {
        ProcessBuilder pb = new ProcessBuilder();
        try { // Inherit error out on JAVA 7 and above
            Class<?> redirect = Class.forName("java.lang.ProcessBuilder$Redirect");
            Object inherit = redirect.getDeclaredField("INHERIT").get(null);
            ProcessBuilder.class.getMethod("redirectError", redirect).invoke(pb, inherit);
            return pb;
        } catch (Throwable e) {
            System.err.println("Java 6 detected - unable to diagnose test process failures: " + e.toString());
        }

        return pb;
    }
}


Question: 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;
/*
 * 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;






/**
 * 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));
Answer:         assertThat(main.getAcquiredLocks(), Matchers.<ThreadLock>empty());