test/jdk/java/net/HugeDataTransferTest.java
author jboes
Fri, 08 Nov 2019 11:15:16 +0000
changeset 59029 3786a0962570
parent 47270 0feb93f627d2
permissions -rw-r--r--
8232853: AuthenticationFilter.Cache::remove may throw ConcurrentModificationException Summary: Change implementation to use iterator instead of plain LinkedList Reviewed-by: dfuchs, vtewari

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

/**
 * @test
 * @bug 8185072
 * @summary network006 times out in many configs in JDK10-hs nightly
 * @run main/othervm/manual HugeDataTransferTest 1
 */
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Random;

/**
 * This test makes huge number of data transfers between 2 Java virtual machines
 * using the TCP/IP protocol, and checks if those data are transfered correctly.
 * Both client and server VMs run on the same local computer and attach TCP/IP
 * sockets to the local host, or to the loopback domain
 * ``<code>localhost</code>'' (having IP address <code>127.0.0.1</code>).
 *
 * <p>
 * In this test, 128 client/server connections are established. Once a
 * connection is established, client passes a data parcel to server, and server
 * reads that parcel and checks if it is same as expected (byte-to-byte equality
 * is desired). Then server passes (some other) parcel to the client, and client
 * reads and verifies those bytes. This ping-pong game is repeated 128 times;
 * and after that each pair of sockets checks if there are no extra bytes
 * accedentally passed through their connection.
 *
 * <p>
 * Parcels lengths and contents are chosen randomly, and average parcel length
 * is 128 bytes. So totally, each pair of sockets passes ~16Kb of data to each
 * other, and thus ~32Kb of data are transfered by each sockets pair. Totally,
 * ~4Mb of data are transfered by all client/server pairs.
 *
 * @author vtewari
 */
public class HugeDataTransferTest {

    /**
     * Timeout for TCP/IP sockets (currently set to 1 min).
     */
    private static int SO_TIMEOUT;// = 2*60*1000;

    /**
     * Maximal number of connections this test should open simultaneously.
     */
    private final static int MAX_CONNECTIONS = 128;

    /**
     * Check few more connections to make sure that MAX_CONNECTIONS are safe.
     */
    private final static int CONNECTIONS_RESERVE = 10;

    /**
     * The test used to fail with connection reset by peer set to 50. (and once
     * in a three if it was set to 10). So now we set it to MAX_CONNECTIONS
     * (128).
     */
    private final static int BACKLOG_QUEUE_LENGTH = MAX_CONNECTIONS;

    /**
     * Number of parcels to be sent/recieved.
     */
    private final static int DATA_PARCELS = 128;

    /**
     * Maximal length of data parcel to be sent/recieved (it equals to 256 bytes
     * now).
     */
    private final static int MAX_PARCEL = 1 << 8;

    /**
     * Either actually display optional reports or not.
     */
    static private final boolean DEBUG_MODE = false;

    /**
     * How many IP sockets can we open simultaneously? Check if
     * <code>MAX_CONNECTIONS</code> connections can be open simultaneously.
     */
    private static int detectOSLimitation() {
        final int CONNECTIONS_TO_TRY = MAX_CONNECTIONS + CONNECTIONS_RESERVE;
        display("--- Trying to open " + CONNECTIONS_TO_TRY + " connections:");

        InetAddress address;
        ServerSocket serverSocket;
        try {
            address = InetAddress.getLocalHost();
            int anyPort = 0;
            int defaultBacklog = BACKLOG_QUEUE_LENGTH;
            serverSocket = new ServerSocket(anyPort, defaultBacklog, address);
        } catch (IOException ioe) {
            throw new Error("FATAL error while loading the test: " + ioe);
        }
        display(serverSocket.toString());

        Socket server[] = new Socket[CONNECTIONS_TO_TRY];
        Socket client[] = new Socket[CONNECTIONS_TO_TRY];

        int i, port = serverSocket.getLocalPort();
        for (i = 0; i < CONNECTIONS_TO_TRY; i++) {
            try {
                client[i] = new Socket(address, port);
                display(">Open: client[" + i + "] = " + client[i]);
                server[i] = serverSocket.accept();
                display(">Open: server[" + i + "] = " + server[i]);
            } catch (IOException ioe) {
                display(">OOPS! -- failed to open connection #" + i);
                break;
            }
        }
        display("> Could open "
                + (i < CONNECTIONS_TO_TRY ? "only " : "") + i + " connections.");
        display(">Closing them:");
        for (int j = 0; j < i; j++) {
            try {
                server[j].close();
                client[j].close();
            } catch (IOException ioe) {
                throw new Error("FATAL error while loading the test: " + ioe);
            }
        }
        display(">OK.");
        int safeConnections = i - CONNECTIONS_RESERVE;
        if (safeConnections < 1) {
            safeConnections = 1;
        }
        if (safeConnections < MAX_CONNECTIONS) {
            complain("------------------------- CAUTION: -------------------");
            complain("While checking the OS limitations, the test found that");
            complain("only " + i + " TCP/IP socket connections could be safely open");
            complain("simultaneously. However, possibility to open at least");
            complain("" + MAX_CONNECTIONS + "+" + CONNECTIONS_RESERVE
                    + " connections were expected.");
            complain("");
            complain("So, the test will check only " + safeConnections + " connection"
                    + (safeConnections == 1 ? "" : "s") + " which seem");
            complain("safe to be open simultaneously.");
            complain("------------------------------------------------------");
        }
        return safeConnections;
    }

    //----------------------------------------------------------------//
    /**
     * Re-calls to the method <code>run(args[],out)</code> actually performing
     * the test. After <code>run(args[],out)</code> stops, follow JDK-like
     * convention for exit codes. I.e.: stop with exit status 95 if the test has
     * passed, or with status 97 if the test has failed.
     *
     * @see #run(String[],PrintStream)
     */
    public static void main(String args[]) {
        int exitCode = run(args, System.out);
        System.exit(exitCode + 95);
        // JCK-like exit status.
    }

    public static int run(String args[], PrintStream out) {
        HugeDataTransferTest.out = out;

        //
        // Get the Internet address of the local machine.
        //
        InetAddress address = null;
        try {
            address = InetAddress.getLocalHost();
        } catch (UnknownHostException exception) {
            complain(exception.toString());
            return 2; // FAILED
        }
        display("Host: " + address);

        //
        // Detect if it is safe to open MAX_CONNETIONS simultaneously:
        //
        final int CONNECTIONS = detectOSLimitation();

        //
        // Assign ServerSocket, and start client VM which should open
        // the prescribed number of CONNECTIONS to that ServerSocket.
        //
        ServerSocket serverSocket;
        try {
            final int anyPort = 0;
            final int defaultBacklog = BACKLOG_QUEUE_LENGTH;
            serverSocket = new ServerSocket(anyPort, defaultBacklog, address);
        } catch (IOException exception) {
            complain("Cannot assign a ServerSocket on: " + address);
            return 2;
        }

        //
        // Start the client process on different VM.
        //
        String jdkPath = System.getProperty("test.jdk");
        Path toolName = Paths.get("bin", "java" + (isWindows() ? ".exe" : ""));
        Path jdkTool = Paths.get(jdkPath, toolName.toString());

        String IPAddress = address.getHostAddress();
        int localPort = serverSocket.getLocalPort();
        String arguments = " " + CONNECTIONS + " " + IPAddress + " " + localPort;
        //String command = args[0] + " " + network006.class.getName() + "$Client " + arguments;
        String command = jdkTool.toAbsolutePath().toString() + " " + Client.class.getName() + " " + arguments;
        try {
            SO_TIMEOUT = Integer.parseInt(args[0]) * 60 * 1000;
        } catch (NumberFormatException e) {
            complain("Wrong timeout argument: " + e);
            return 2;
        }

        Runtime runtime = Runtime.getRuntime();

        Process client = null;
        IORedirector redirectOut = null;
        IORedirector redirectErr = null;

        try {
            // Start clients on different JVM:
            client = runtime.exec(command);

            // Provide clients with access to stderr and stdout:
            InputStream clientOut = client.getInputStream();
            InputStream clientErr = client.getErrorStream();
            redirectOut = new IORedirector(clientOut, DEBUG_MODE ? out : null);
            redirectErr = new IORedirector(clientErr, out);
            redirectOut.start();
            redirectErr.start();

        } catch (IOException exception) {
            complain("Failed to start client: " + exception);
            return 2;
        }
        //
        // Start the server threads (and let them establish connections):
        //

        Server server[] = new Server[CONNECTIONS];
        for (int i = 0; i < CONNECTIONS; i++) {
            server[i] = new Server(serverSocket);
            display("Server #" + i + ": " + server[i]);
            server[i].start();
        }

        //
        // Wait for the servers and the clients:
        //
        boolean testFailed = false;

        try {
            client.waitFor();
            int clientStatus = client.exitValue();
            display("Client VM exitCode=" + clientStatus);

            // Let I/O redirectors to flush:
            if (redirectOut.isAlive()) {
                redirectOut.join();
            }
            if (redirectErr.isAlive()) {
                redirectErr.join();
            }

            // If client has crashed, also terminate the server (to avoid hangup).
            if (clientStatus != 95) {
                complain("Client VM has crashed: exit status=" + clientStatus);
                testFailed = true;
            }

            // Client has finished OK; wait for the server.
            for (int i = 0; i < CONNECTIONS; i++) {
                display("Server: waiting for #" + i);
                if (server[i].isAlive()) {
                    display("Server #" + i + ": (joining...)" + server[i]);
                    server[i].join();
                }
                if (server[i].exception != null) {
                    if (server[i].message != null) {
                        complain("Server #" + i + "(finished): with message:" + server[i].message);
                    }

                    complain("Server #" + i + "(finished): " + server[i].exception);
                    server[i].exception.printStackTrace(out);
                    out.flush();
//                    complain("Server #"+i+": "+server[i].exception.getStackTrace());
                    testFailed = true;
                }
            }

        } catch (InterruptedException exception) {
            complain("Test interrupted: " + exception);
            testFailed = true;
        }

        if (testFailed) {
            complain("Test failed.");
        } else {
            display("Test passed.");
        }
        return testFailed ? 2 : 0;
    }

    private static boolean isWindows() {
        return System.getProperty("os.name").toLowerCase().startsWith("win");
    }
    //----------------------------------------------------------------//
    /**
     * Log stream for error messages and/or (optional) execution trace.
     */
    private static PrintStream out;

    /**
     * Print error message.
     */
    private static synchronized void complain(Object message) {
        out.println("# " + message);
        out.flush();
    }

    ;

    /**
     * Display optional report: comment ca va?
     */
    private static synchronized void display(Object report) {
        if (DEBUG_MODE) {
            out.println(report.toString());
        }
        out.flush(); //todo shouldn't this be inside if??
    }

    ;

    //----------------------------------------------------------------//

    /**
     * Server thread should reply to data parcels sent by Client VM.
     */
    private static class Server extends Thread {

        /**
         * The socket is assigned at the Server instantiation.
         */
        private ServerSocket serverSocket;

        /**
         * The socket is assigned at the Server runtime.
         */
        private Socket socket;

        /**
         * Display the server socket.
         */
        @Override
        public String toString() {

            return "ServerSocket: " + serverSocket.toString();
//                    + " socket: " + socket.toString();
        }

        /**
         * Which port is this socket listening?
         */
        int getPort() {
            return serverSocket.getLocalPort();
        }

        /**
         * Find some free port at the given <code>address</code> and attach new
         * server to hear that port. // lidsten to??
         */
        public Server(ServerSocket serverSocket) {
            this.serverSocket = serverSocket;
        }

        /**
         * Exception just arisen while the server was working, or
         * <code>null</code> if it was OK with the server.
         */
        Exception exception = null;
        String message = null;

        /**
         * Accept connection, then reply to client's parcels.
         */
        @Override
        public void run() {
            try {
                socket = serverSocket.accept();
                socket.setSoTimeout(SO_TIMEOUT);

                InputStream istream = socket.getInputStream();
                OutputStream ostream = socket.getOutputStream();

                Random random = new Random(getPort());

                for (int i = 0; i < DATA_PARCELS; i++) {
                    Parcel etalon = new Parcel(random);
                    message = "reading parcel number " + i;
                    Parcel sample = new Parcel(istream); // read
                    if (!sample.equals(etalon)) {
                        complain("Server thread for port #"
                                + getPort() + " got unexpected parcel:\n"
                                + "sample=" + sample + "\n"
                                + "etalon=" + etalon);
                        throw new TestFailure( //received??
                                "server has read unexpected parcel");
                    }
                    message = "sending parcel number " + i;
                    etalon.send(ostream);
                    ostream.flush();
                }

                int datum = istream.read(); // wait for client close()
                if (datum >= 0) {
                    throw new TestFailure(
                            "server has read ambigous byte: " + datum);
                }

                ostream.close(); // implies: socket.close();

            } catch (Exception oops) {
                exception = oops;
            }
        }
    }

    //----------------------------------------------------------------//
    /**
     * Client VM should send data parcels to Server VM and recieve and verify
     * the server's replies.
     */
    private static class Client extends Thread {

        /**
         * This thread uses the single client socket.
         */
        private Socket socket;

        /**
         * Address and port of this socket.
         */
        @Override
        public String toString() {
            return socket.toString();
        }

        /**
         * Did the thread failed? If yes, what is the failure's reason.
         */
        Exception exception = null;
        String message = null;

        public static java.io.PrintStream complainStream = System.out;
        public static java.io.PrintStream displayStream = System.err;

        /**
         * Connect client socket on the given <code>address</code> and
         * <code>port</code>.
         */
        Client(InetAddress address, int port) throws IOException {
            socket = new Socket(address, port);
            socket.setSoTimeout(SO_TIMEOUT);
        }

        /**
         * What is the port number this socket is listening for?
         */
        int getPort() {
            return socket.getPort();
        }

        /**
         * Establish connection, then read/respond <code>DATA_PARCELS</code>
         * parcels of random data. Set initial seed for pseudo-random numbers
         * generator to the value of the local port number.
         *
         * @see #DATA_PARCELS
         * @see #getPort()
         */
        @Override
        public void run() {
            try {
                InputStream istream = socket.getInputStream();
                OutputStream ostream = socket.getOutputStream();

                Random random = new Random(getPort());
                // suggested by Oleg -- to avoid race conditions
                /* try{
                    Thread.sleep(500);
                }
                catch (java.lang.InterruptedException e)
                {
                }*/

                for (int i = 0; i < DATA_PARCELS; i++) {
                    Parcel etalon = new Parcel(random);
                    message = "sending parcel number: " + i;
                    etalon.send(ostream);
                    ostream.flush();

                    message = "reading parcel number: " + i;
                    Parcel sample = new Parcel(istream); // read
                    if (!sample.equals(etalon)) {
                        complain("Client thread for port #"
                                + getPort() + " got unexpected parcel:\n"
                                + "sample=" + sample + "\n"
                                + "etalon=" + etalon);
                        throw new TestFailure(
                                "parcel context is unexpected to client");
                    }
                }

                if (istream.available() > 0) {
                    int datum = istream.read();
                    throw new TestFailure(
                            "client has read ambigous byte: " + datum);
                }
                ostream.close(); // implies: socket.close()

            } catch (Exception oops) {
                exception = oops;
            }
        }

        /**
         * Establish lots of connections to server socket, attack servers with
         * huge data parcels, and check if they reply correctly. The number of
         * connections to try, the address and port number for the server socket
         * are passed through <code>args[]</code>, like:
         * <pre>
         *    java network006$Client connections_to_try address port
         * </pre>
         */
        public static void main(String args[]) {
            if (DEBUG_MODE) {
                try {
                    String filename = "Client" + ((args.length == 3) ? args[2] : "new");
                    displayStream = new PrintStream(filename + ".out");
                    complainStream = new PrintStream(filename + ".err");
                } catch (FileNotFoundException exception) {
                    complain(exception);
                }

            }

            if (args.length != 3) {
                complain("Client expects 3 paramenets:");
                complain("    java " + Client.class.getName() + " connections_to_try address port");
                exit(1); // FAILED
            }

            int CONNECTIONS = Integer.parseInt(args[0]);
            display("Client VM: will try " + CONNECTIONS + " connections.");
            InetAddress address;
            try {
                address = InetAddress.getByName(args[1]);
            } catch (UnknownHostException exception) {
                address = null;
                complain("Client: cannot find host: \"" + args[1] + "\"");
                exit(4);
            }
            display("Client: host to contact: " + address);
            int port = Integer.parseInt(args[2]);
            display("Client: port to contact: " + port);

            //
            // Establish connections, and start client processes:
            //
            Client client[] = new Client[CONNECTIONS];
            for (int i = 0; i < CONNECTIONS; i++) {
                try {
                    client[i] = new Client(address, port);
                    display("Client #" + i + ": " + client[i]);

                } catch (IOException ioe) {
                    complain("Client #" + i + "(creation): " + ioe);
                    ioe.printStackTrace(complainStream);
                    complainStream.flush();
//                    complain("Client #" + i + "(creation): " + ioe.getStackTrace());
                    exit(3);
                }
            }

            for (int i = 0; i < CONNECTIONS; i++) {
                client[i].start();
            }

            //
            // Wait until testing is not finished:
            //
            int status = 0;
            for (int i = 0; i < CONNECTIONS; i++) {
                display("Client: waiting for #" + i);
                if (client[i].isAlive()) {
                    display("Client #" + i + ": (joining...)" + client[i]);

                    try {
                        client[i].join();
                    } catch (InterruptedException ie) {
                        complain("Client #" + i + ": " + ie);
                        status = 3;
                    }
                }
                if (client[i].exception != null) {
                    if (client[i].message != null) {
                        complain("Client #" + i + "(finished) with message: " + client[i].message);
                    }
                    complain("Client #" + i + "(finished): " + client[i].exception);
                    client[i].exception.printStackTrace(complainStream);
                    complainStream.flush();
                    if (status == 0) {
                        status = 2;
                    }
                }
            }

            exit(status);
        }

        /**
         * Print error message.
         */
        private static synchronized void complain(Object message) {
            complainStream.println("# " + message);
            complainStream.flush();
        }

        /**
         * Display execution trace.
         */
        private static synchronized void display(Object message) {
            if (!DEBUG_MODE) {
                return;
            }
            displayStream.println(message.toString());
            displayStream.flush();
        }

        /**
         * Exit with JCK-like status.
         */
        private static void exit(int exitCode) {
            int status = exitCode + 95;
//          display("Client: exiting with code=" + status);
            System.exit(status);
        }
    }

    /**
     * Two of such threads should redirect <code>out</code> and <code>err</code>
     * streams of client VM.
     */
    private static class IORedirector extends Thread {

        /**
         * Source stream.
         */
        InputStream in;
        /**
         * Destination stream.
         */
        OutputStream out;

        /**
         * Redirect <code>in</code> to <code>out</code>.
         */
        public IORedirector(InputStream in, OutputStream out) {
            this.in = in;
            this.out = out;
        }

        /**
         * Read input stream until the EOF, and write everithing to output
         * stream. If output stream is assigned to <code>null</code>, do not
         * print anything, but read the input stream anywhere.
         */
        @Override
        public void run() {
            try {
                for (;;) {
                    int symbol = in.read();
                    if (symbol < 0) {
                        break; // EOF
                    }
                    if (out != null) {
                        out.write(symbol);
                    }
                }

                if (out != null) {
                    out.flush();
                }

            } catch (IOException exception) {
                throw new TestFailure("IORedirector exception: " + exception);
            }
        }
    }

    //----------------------------------------------------------------//
    /**
     * A data parcel to be sent/recieved between Client VM and Server thread.
     * When data parcel is sent, first 4 bytes are transfered which encode the
     * <code>int</code> number equal to size of the parcel minus 1. I.e.: if
     * number of data bytes in the parcel's contents is <code>N</code>, then the
     * first 4 bytes encode the number <code>N-1</code>. After that, the
     * parcel's contents bytes are transered.
     */
    static class Parcel {

        private final byte[] parcel;

        /**
         * Display all bytes as integer values from 0 to 255; or return
         * ``<tt>null</tt>'' if this Parcel is not yet initialized.
         */
        @Override
        public String toString() {
            if (parcel == null) {
                return "null";
            }
            String s = "{";
            for (int i = 0; i < parcel.length; i++) {
                s += (i > 0 ? ", " : "") + ((int) parcel[i] & 0xFF);
            }
            return s + "}";
        }

        /**
         * Generate new <code>parcel[]</code> array using the given
         * <code>random</code> numbers generator. Client and Server threads
         * should use identical <code>random</code> generators, so that those
         * threads could generate equal data parcels and check the parcel just
         * transfered.
         */
        public Parcel(Random random) {
            int size = random.nextInt(MAX_PARCEL) + 1;
            parcel = new byte[size];
            for (int i = 0; i < size; i++) {
                parcel[i] = (byte) random.nextInt(256);
            }
        }

        ;

        /**
         * Read exactly <code>size</code> bytes from the <code>istream</code>
         * if possible, or throw <code>TestFailure</code> if unexpected end of
         * <code>istream</code> occurs.
         */
        private static byte[] readBytes(int size, InputStream istream)
                throws IOException {

            byte data[] = new byte[size];
            for (int i = 0; i < size; i++) {
                int datum = istream.read();
                if (datum < 0) {
                    throw new TestFailure(
                            "unexpected EOF: have read: " + i + " bytes of " + size);
                }
                data[i] = (byte) datum;
            }
            return data;
        }

        /**
         * Read 4 bytes from <code>istream</code> and threat them to encode size
         * of data parcel following these 4 bytes.
         */
        private static int getSize(InputStream istream) throws IOException {
            byte data[] = readBytes(4, istream);
            int data0 = (int) data[0] & 0xFF;
            int data1 = (int) data[1] & 0xFF;
            int data2 = (int) data[2] & 0xFF;
            int data3 = (int) data[3] & 0xFF;
            int sizeWord = data0 + (data1 << 8) + (data2 << 16) + (data3 << 24);
            int size = sizeWord + 1;
            if (size <= 0) {
                throw new TestFailure("illegal size: " + size);
            }
            return size;
        }

        /**
         * Send 4 bytes encoding actual size of the parcel just to be
         * transfered.
         */
        private static void putSize(OutputStream ostream, int size)
                throws IOException {

            if (size <= 0) {
                throw new TestFailure("illegal size: " + size);
            }

            int sizeWord = size - 1;
            byte data[] = new byte[4];
            data[0] = (byte) sizeWord;
            data[1] = (byte) (sizeWord >> 8);
            data[2] = (byte) (sizeWord >> 16);
            data[3] = (byte) (sizeWord >> 24);
            ostream.write(data);
        }

        /**
         * Recieve data parcel.
         */
        public Parcel(InputStream istream) throws IOException {
            int size = getSize(istream);
            parcel = readBytes(size, istream);
        }

        /**
         * Send <code>this</code> data parcel.
         */
        public void send(OutputStream ostream) throws IOException {
            int size = parcel.length;
            putSize(ostream, size);
            ostream.write(parcel);
        }

        /**
         * Check byte-to-byte equality between <code>this</code> and the
         * <code>other</code> parcels.
         */
        public boolean equals(Parcel other) {
            if (this.parcel.length != other.parcel.length) {
                return false;
            }
            int size = parcel.length;
            for (int i = 0; i < size; i++) {
                if (this.parcel[i] != other.parcel[i]) {
                    return false;
                }
            }
            return true;
        }
    }

    /**
     * Server or Client may throw this exception to report the test failure.
     */
    static class TestFailure extends RuntimeException {

        /**
         * Report particular <code>purpose</code> of the test failure.
         */
        public TestFailure(String purpose) {
            super(purpose);
        }
    }
}