All Downloads are FREE. Search and download functionalities are using the official Maven repository.

tests.java.javatests.Issue1972 Maven / Gradle / Ivy

Go to download

Jython is an implementation of the high-level, dynamic, object-oriented language Python written in 100% Pure Java, and seamlessly integrated with the Java platform. It thus allows you to run Python on any Java platform.

There is a newer version: 2.7.4
Show newest version
// Copyright (c)2013 Jython Developers
package javatests;

import static org.junit.Assert.*;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.regex.Pattern;

import org.junit.After;
import org.junit.Test;

/**
 * Tests investigating issues with readline() first raised in Jython Issue #1972. These involve
 * sub-process input and output through the console streams. Although the console streams are used,
 * the JLine console handler is not engaged, as the test {@link #jythonJLineConsole()} verifies. You
 * could run this as a straight JUnit test, or in various debugging configurations, including remote
 * debugging of the subprocess.
 * 

* This test passes in Jython 2.5.2 and 2.5.4rc1. The test {@link #jythonReadline()} fails with * Jython 2.5.3. *

* The bulk of this program is designed to be run as JUnit tests, but it also has a * {@link #main(String[])} method that echoes System.in onto System.out * either as byte data (characters effectively) or as hexadecimal. The early tests run this as a * subprocess to establish exactly what bytes, in particular exactly what line endings, are received * and emitted by a simple Java subprocess. The later tests run Jython as the subprocess, executing * various command line programs and interactive console commands. *

* This was developed on Windows. It tries to abstract away the particular choice of line separator, * so it will run on other platforms, but it hasn't been tested that this was successful. */ public class Issue1972 { /** Set to non-zero port number to enable subprocess debugging in selected tests. */ static int DEBUG_PORT = 0; // 8000 or 0 /** Control the amount of output to the console: 0, 1 or 2. */ static int VERBOSE = 0; /** Lines in stdout (as regular expressions) to ignore when checking subprocess output. */ static String[] STDOUT_IGNORE = {"Listening for transport dt_socket"}; /** Lines in stderr (as regular expressions) to ignore when checking subprocess output. */ static String[] STDERR_IGNORE = {"Jython 2", "\\*sys-package-mgr"}; /** * Extra JVM options used when debugging is enabled. DEBUG_PORT will be substituted * for the %d marker in a String.format call. The debugger must attach * to the application after it is launched by this test programme. * */ static final String DEBUG_OPTS = "-agentlib:jdwp=transport=dt_socket,server=y,address=%d,suspend=y"; /** Subprocess created by {@link #setProcJava(String...)} */ private Process proc = null; /** The stdin of the subprocess as a writable stream. */ private OutputStream toProc; /** A queue handling the stdout of the subprocess. */ private LineQueue inFromProc; /** A queue handling the stderr of the subprocess. */ private LineQueue errFromProc; @After public void afterEachTest() { if (proc != null) { proc.destroy(); } inFromProc = errFromProc = null; } static final Properties props = System.getProperties(); static final String lineSeparator = props.getProperty("line.separator"); static final String pythonHome = props.getProperty("python.home"); /** * Check that on this system we know how to launch and read the error output from a subprocess. * * @throws IOException */ @Test public void readStderr() throws Exception { announceTest(VERBOSE, "readStderr"); // Run java -version, which outputs on System.err setProcJava("-version"); proc.waitFor(); // Dump to console outputAsHexDump(VERBOSE, inFromProc, errFromProc); assertEquals("Unexpected text in stdout", 0, inFromProc.size()); assertTrue("No text output to stderr", errFromProc.size() > 0); String res = errFromProc.asStrings().get(0); assertTrue("stderr should mention version", res.contains("version")); } /** * Check that on this system we know how to launch and read standard output from a subprocess. * * @throws IOException */ @Test public void readStdout() throws Exception { announceTest(VERBOSE, "readStdout"); // Run the main of this class setProcJava(this.getClass().getName()); proc.waitFor(); outputAsHexDump(VERBOSE, inFromProc, errFromProc); checkErrFromProc(); checkInFromProc("Hello"); } /** * Check that on this system we know how to launch, write to and read from a subprocess. * * @throws IOException */ @Test public void echoStdin() throws Exception { announceTest(VERBOSE, "echoStdin"); // Run the main of this class as an echo programme setProcJava(this.getClass().getName(), "echo"); writeToProc("spam"); writeToProc("spam\r"); writeToProc("spam\r\n"); toProc.close(); proc.waitFor(); outputAsHexDump(VERBOSE, inFromProc, errFromProc); checkErrFromProc(); checkInFromProc(false, "Hello\\r\\n", "spamspam\\r", "spam\\r\\n"); } /** * Check that on this system line endings are received as expected by a subprocess. *

* Observation from Windows 7 x64: data is written to the subprocess once * flush() is called on the output stream. It can be read from * System.in in the subprocess, which of course writes hex to * System.out but that data is not received back in the parent process until * System.out.println() is called in the subprocess. * * @throws IOException */ @Test public void echoStdinAsHex() throws Exception { announceTest(VERBOSE, "echoStdinAsHex"); // Run the main of this class as an echo programme setProcJava(this.getClass().getName(), "hex"); writeToProc("a\r"); writeToProc("b\n"); writeToProc("c\r\n"); toProc.close(); proc.waitFor(); outputAsStrings(VERBOSE, inFromProc, errFromProc); checkErrFromProc(); checkInFromProc("Hello", " 61", " 0d", " 62", " 0a", " 63", " 0d", " 0a"); } /** * Test reading back from Jython subprocess with program on command-line. * * @throws Exception */ @Test public void jythonSubprocess() throws Exception { announceTest(VERBOSE, "jythonSubprocess"); // Run Jython hello programme setProcJava("org.python.util.jython", "-c", "print 'Hello'"); proc.waitFor(); outputAsHexDump(VERBOSE, inFromProc, errFromProc); checkErrFromProc(); checkInFromProc("Hello"); } /** * Discover what is handling the "console" when the program is on the command line only. * * @throws Exception */ @Test public void jythonNonInteractive() throws Exception { announceTest(VERBOSE, "jythonNonInteractiveConsole"); // Run Jython enquiry about console as -c program setProcJava("org.python.util.jython", "-c", "import sys; print type(sys._jy_console).__name__; print sys.stdin.isatty()"); proc.waitFor(); outputAsStrings(VERBOSE, inFromProc, errFromProc); checkErrFromProc(); checkInFromProc("PlainConsole", "False"); } /** * Discover what is handling the "console" when the program is entered interactively at the * Jython prompt. * * @throws Exception */ @Test public void jythonInteractive() throws Exception { announceTest(VERBOSE, "jythonInteractiveConsole"); // Run Jython with simple actions at the command prompt setProcJava( // "-Dpython.home=" + pythonHome, // "org.python.util.jython"); writeToProc("12+3\n"); writeToProc("import sys\n"); writeToProc("print type(sys._jy_console).__name__\n"); writeToProc("print sys.stdin.isatty()\n"); toProc.close(); proc.waitFor(); outputAsStrings(VERBOSE, inFromProc, errFromProc); checkErrFromProc(""); // stderr produces one empty line. Why? checkInFromProc("15", "PlainConsole", "False"); } /** * Discover what is handling the "console" when the program is entered interactively at the * Jython prompt, and we try to force use of JLine (which fails). * * @throws Exception */ @Test public void jythonJLineConsole() throws Exception { announceTest(VERBOSE, "jythonJLineConsole"); // Run Jython with simple actions at the command prompt setProcJava( // "-Dpython.console=org.python.util.JLineConsole", // "-Dpython.home=" + pythonHome, // "org.python.util.jython"); writeToProc("12+3\n"); writeToProc("import sys\n"); writeToProc("print type(sys._jy_console).__name__\n"); writeToProc("print sys.stdin.isatty()\n"); toProc.close(); proc.waitFor(); outputAsStrings(VERBOSE, inFromProc, errFromProc); checkErrFromProc(""); // stderr produces one empty line. Why? // We can specify JLineConsole, but isatty() is not fooled. checkInFromProc("15", "PlainConsole", "False"); } /** * Test writing to and reading back from Jython subprocess with echo program on command-line. * * @throws Exception */ @Test public void jythonReadline() throws Exception { announceTest(VERBOSE, "jythonReadline"); // Run Jython simple readline programme setProcJava( // "-Dpython.console=org.python.util.JLineConsole", // // "-Dpython.console.interactive=True", // "-Dpython.home=" + pythonHome, // "org.python.util.jython", // "-c", // "import sys; sys.stdout.write(sys.stdin.readline()); sys.stdout.flush();" // ); // Discard first output (banner or debugging sign-on) inFromProc.clear(); errFromProc.clear(); // Force lines in until something comes out or it breaks String spamString = "spam" + lineSeparator; byte[] spam = (spamString).getBytes(); int count, limit = 9000; for (count = 0; count <= limit; count += spam.length) { toProc.write(spam); toProc.flush(); // Give the sub-process a chance the first time and the last if (count == 0 || count + spam.length > limit) { Thread.sleep(10000); } // If anything came back, we're done if (inFromProc.size() > 0) { break; } if (errFromProc.size() > 0) { break; } } if (VERBOSE >= 1) { System.out.println(String.format(" count = %4d", count)); } toProc.close(); proc.waitFor(); outputAsHexDump(VERBOSE, inFromProc, errFromProc); assertTrue("Subprocess did not respond promptly to first line", count == 0); checkInFromProc("spam"); } /** * A main program that certain tests in the module will use as a subprocess. If an argument is * given it means: *

* * * * * * * * *
"echo"echo the characters received as text
"hex"echo the characters as hexadecimal
* * @param args * @throws IOException */ public static void main(String args[]) throws IOException { System.out.println("Hello"); if (args.length > 0) { String arg = args[0]; if ("echo".equals(arg)) { int c; while ((c = System.in.read()) != -1) { System.out.write(c); System.out.flush(); } } else if ("hex".equals(arg)) { int c; while ((c = System.in.read()) != -1) { System.out.printf(" %02x", c); System.out.println(); } } else { System.err.println("Huh?"); } } } /** * Invoke the java command with the given arguments. The class path will be the same as this * programme's class path (as in the property java.class.path). * * @param args further arguments to the program run * @return the running process * @throws IOException */ static Process startJavaProcess(String... args) throws IOException { // Prepare arguments for the java command String javaClassPath = props.getProperty("java.class.path"); List cmd = new ArrayList(); cmd.add("java"); cmd.add("-classpath"); cmd.add(javaClassPath); if (DEBUG_PORT > 0) { cmd.add(String.format(DEBUG_OPTS, DEBUG_PORT)); } for (String arg : args) { cmd.add(arg); } // Create the factory for the external process with the given command ProcessBuilder pb = new ProcessBuilder(); pb.command(cmd); // If you want to check environment variables, it looks like this: /* * Map env = pb.environment(); for (Map.Entry entry : * env.entrySet()) { System.out.println(entry); } */ // Actually create the external process and return the Java object representing it return pb.start(); } /** * Invoke the java command with the given arguments. The class path will be the same as this * programme's class path (as in the property java.class.path). After the call, * {@link #proc} references the running process and {@link #inFromProc} and {@link #errFromProc} * are handling the stdout and stderr of the subprocess. * * @param args further arguments to the program run * @throws IOException */ private void setProcJava(String... args) throws IOException { proc = startJavaProcess(args); inFromProc = new LineQueue(proc.getInputStream()); errFromProc = new LineQueue(proc.getErrorStream()); toProc = proc.getOutputStream(); } /** * Write this string into the stdin of the subprocess. The platform default * encoding will be used. * * @param s to write * @throws IOException */ private void writeToProc(String s) throws IOException { toProc.write(s.getBytes()); toProc.flush(); } /** * Check lines of {@link #queue} against expected text. Lines from the subprocess, after the * {@link #escape(byte[])} transormation has been applied, are expected to be equal to the * strings supplied, optionally after {@link #escapedSeparator} has been added to the expected * strings. * * @param message identifies the queue in error message * @param addSeparator if true, system-defined line separator expected * @param queue to be compared * @param toIgnore patterns defining lines to ignore while processing * @param expected lines of text (given without line separators) */ private void checkFromProc(String message, boolean addSeparator, LineQueue queue, List toIgnore, String... expected) { if (addSeparator) { // Each expected string must be treated as if the lineSeparator were appended String escapedSeparator = ""; try { escapedSeparator = escape(lineSeparator.getBytes("US-ASCII")); } catch (UnsupportedEncodingException e) { fail("Could not encode line separator as ASCII"); // Never happens } // ... so append one for (int i = 0; i < expected.length; i++) { expected[i] += escapedSeparator; } } // Get the escaped form of the byte buffers in the queue List results = queue.asStrings(); // Count through the results, comparing what we can't ignore to what was expected int count = 0; for (String r : results) { if (!beginsWithAnyOf(r, toIgnore)) { if (count < expected.length) { // Check the line against the expected text assertEquals(message, expected[count++], r); } else { // Extra line will be a failure but continue to count count++; } } } // Check number of lines we can't ignore against the number expected assertEquals(message, expected.length, count); } /** Compiled regular expressions for the lines to ignore (on stdout). */ private static List stdoutIgnore; /** Compiled regular expressions for the lines to ignore (on stderr). */ private static List stderrIgnore; /** If not already done, compile the regular expressions we need. */ private static void compileToIgnore() { if (stdoutIgnore == null || stderrIgnore == null) { // Compile the lines to ignore to Pattern objects stdoutIgnore = compileAll(STDOUT_IGNORE); stderrIgnore = compileAll(STDERR_IGNORE); } } /** If not already done, compile one set of regular expressions to patterns. */ private static List compileAll(String[] regex) { List result = new LinkedList(); if (regex != null) { for (String s : regex) { Pattern p = Pattern.compile(s); result.add(p); } } return result; } /** * Compute whether a string begins with any of a set of strings. * * @param s the string in question * @param patterns to check against * @return */ private static boolean beginsWithAnyOf(String s, List patterns) { for (Pattern p : patterns) { if (p.matcher(s).lookingAt()) { return true; } } return false; } /** * Check lines of {@link #inFromProc} against expected text. * * @param addSeparator if true, system-defined line separator expected * @param expected lines of text (given without line separators) */ private void checkInFromProc(boolean addSeparator, String... expected) { compileToIgnore(); // Make sure we have the matcher patterns checkFromProc("subprocess stdout", addSeparator, inFromProc, stdoutIgnore, expected); } /** * Check lines of {@link #inFromProc} against expected text. Lines from the subprocess are * expected to be equal to those supplied after {@link #escapedSeparator} has been added. * * @param expected lines of text (given without line separators) */ private void checkInFromProc(String... expected) { checkInFromProc(true, expected); } /** * Check lines of {@link #errFromProc} against expected text. * * @param addSeparator if true, system-defined line separator expected * @param expected lines of text (given without line separators) */ private void checkErrFromProc(boolean addSeparator, String... expected) { compileToIgnore(); // Make sure we have the matcher patterns checkFromProc("subprocess stderr", addSeparator, errFromProc, stderrIgnore, expected); } /** * Check lines of {@link #errFromProc} against expected text. Lines from the subprocess are * expected to be equal to those supplied after {@link #escapedSeparator} has been added. * * @param expected lines of text (given without line separators) */ private void checkErrFromProc(String... expected) { checkErrFromProc(true, expected); } /** * Brevity for announcing tests on the console when that is used to dump values. * * @param verbose if <1 suppress output * @param name of test */ static void announceTest(int verbose, String name) { if (verbose >= 1) { System.out.println(String.format("------- Test: %-40s -------", name)); } } /** * Output is System.out the formatted strings representing lines from a subprocess stdout. * * @param verbose if <2 suppress output * @param inFromProc lines received from the stdout of a subprocess */ static void outputAsStrings(int verbose, LineQueue inFromProc) { if (verbose >= 2) { outputStreams(inFromProc.asStrings(), null); } } /** * Output is System.out the formatted strings representing lines from a subprocess stdout, and * if there are any, from stderr. * * @param verbose if <2 suppress output * @param inFromProc lines received from the stdout of a subprocess * @param errFromProc lines received from the stderr of a subprocess */ static void outputAsStrings(int verbose, LineQueue inFromProc, LineQueue errFromProc) { if (verbose >= 2) { outputStreams(inFromProc.asStrings(), errFromProc.asStrings()); } } /** * Output is System.out a hex dump of lines from a subprocess stdout. * * @param verbose if <2 suppress output * @param inFromProc lines received from the stdout of a subprocess */ static void outputAsHexDump(int verbose, LineQueue inFromProc) { if (verbose >= 2) { outputStreams(inFromProc.asHexDump(), null); } } /** * Output is System.out a hex dump of lines from a subprocess stdout, and if there are any, from * stderr. * * @param verbose if <2 suppress output * @param inFromProc lines received from the stdout of a subprocess * @param errFromProc lines received from the stderr of a subprocess */ static void outputAsHexDump(int verbose, LineQueue inFromProc, LineQueue errFromProc) { if (verbose >= 2) { outputStreams(inFromProc.asHexDump(), errFromProc.asHexDump()); } } /** * Output is System.out the formatted strings representing lines from a subprocess stdout, and * if there are any, from stderr. * * @param stdout to output labelled "Output stream:" * @param stderr to output labelled "Error stream:" unless an empty list or null */ private static void outputStreams(List stdout, List stderr) { PrintStream out = System.out; out.println("Output stream:"); for (String line : stdout) { out.println(line); } if (stderr != null && stderr.size() > 0) { out.println("Error stream:"); for (String line : stderr) { out.println(line); } } } private static final String ESC_CHARS = "\r\n\t\\\b\f"; private static final String[] ESC_STRINGS = {"\\r", "\\n", "\\t", "\\\\", "\\b", "\\f"}; /** * Helper to format one line of string output hex-escaping non-ASCII characters. * * @param sb to overwrite with the line of dump output * @param bb from which to take the bytes */ private static void stringDump(StringBuilder sb, ByteBuffer bb) { // Reset the string buffer sb.setLength(0); int n = bb.remaining(); for (int i = 0; i < n; i++) { // Read byte as character code (old-style ascii mindset at work here) char c = (char)(0xff & bb.get()); // Check for C-style escape characters int j = ESC_CHARS.indexOf(c); if (j >= 0) { // Use replacement escape sequence sb.append(ESC_STRINGS[j]); } else if (c < ' ' || c > 126) { // Some non-printing character that doesn't have an escape sb.append(String.format("\\x%02x", c)); } else { // A safe character sb.append(c); } } } /** * Convert bytes (interpreted as ASCII) to String where the non-ascii characters are escaped. * * @param b * @return */ public static String escape(byte[] b) { StringBuilder sb = new StringBuilder(100); ByteBuffer bb = ByteBuffer.wrap(b); stringDump(sb, bb); return sb.toString(); } /** * Wrapper for an InputStream that creates a thread to read it in the background into a queue of * ByteBuffer objects. Line endings (\r, \n or \r\n) are preserved. This is used in the tests to * see exactly what a subprocess produces, without blocking the subprocess as it writes. The * data are available as a hexadecimal dump (a bit like od) and as string, assuming * a UTF-8 encoding, or some subset like ASCII. */ static class LineQueue extends LinkedBlockingQueue { static final int BUFFER_SIZE = 1024; private InputStream in; ByteBuffer buf; boolean seenCR; Thread scribe; /** * Wrap a stream in the reader and immediately begin reading it. * * @param in */ LineQueue(InputStream in) { this.in = in; scribe = new Thread() { @Override public void run() { try { runScribe(); } catch (IOException e) { e.printStackTrace(); } } }; // Set the scribe thread off filling buffers scribe.start(); } /** * Scan every byte read from the input and squirrel them away in buffers, one per line, * where lines are delimited by \r, \n or \r\n.. * * @throws IOException */ private void runScribe() throws IOException { int c; newBuffer(); while ((c = in.read()) != -1) { byte b = (byte)c; if (c == '\n') { // This is always the end of a line buf.put(b); emitBuffer(); newBuffer(); } else if (seenCR) { // The line ended just before the new character emitBuffer(); newBuffer(); buf.put(b); } else if (c == '\r') { // This may be the end of a line (if next is not '\n') buf.put(b); seenCR = true; } else { // Not the end of a line, just accumulate buf.put(b); } } // Emit a partial line if there is one. if (buf.position() > 0) { emitBuffer(); } } private void newBuffer() { buf = ByteBuffer.allocate(BUFFER_SIZE); seenCR = false; } private void emitBuffer() { buf.flip(); add(buf); } /** * Return the contents of the queue as a list of escaped strings, interpreting the bytes as * ASCII. * * @return contents as strings */ public List asStrings() { // Make strings here: StringBuilder sb = new StringBuilder(100); // Build a list of decoded buffers List list = new LinkedList(); for (ByteBuffer bb : this) { stringDump(sb, bb); list.add(sb.toString()); bb.rewind(); } return list; } /** * Return a hex dump the contents of the object as a list of strings * * @return dump as strings */ public List asHexDump() { final int LEN = 16; StringBuilder sb = new StringBuilder(4 * LEN + 20); // Build a list of dumped buffer rows List list = new LinkedList(); for (ByteBuffer bb : this) { int n; while ((n = bb.remaining()) >= LEN) { hexDump(sb, bb, n, LEN); list.add(sb.toString()); } if (n > 0) { hexDump(sb, bb, n, LEN); list.add(sb.toString()); } bb.rewind(); } return list; } /** * Helper to format one line of hex dump output up to a maximum number of bytes. * * @param sb to overwrite with the line of dump output * @param bb from which to take the bytes * @param n number of bytes to take (up to len) * @param len maximum number of bytes to take */ private static void hexDump(StringBuilder sb, ByteBuffer bb, int n, int len) { // Reset the string buffer sb.setLength(0); // Impose the limit if (n > len) { n = len; } // The data on this row of output start here in the ByteBuffer bb.mark(); sb.append(String.format("%4d: ", bb.position())); // Output n of them for (int i = 0; i < n; i++) { sb.append(String.format(" %02x", bb.get())); } // And make it up to the proper width for (int i = n; i < len; i++) { sb.append(" "); } // Now go back to the start of the row and output printable characters bb.reset(); sb.append("|"); for (int i = 0; i < n; i++) { char c = (char)(0xff & bb.get()); if (c < ' ' || c > 126) { c = '.'; } sb.append(c); } } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy