uk.ac.diamond.scisoft.analysis.PythonHelper Maven / Gradle / Ivy
/*
* Copyright (c) 2012 Diamond Light Source Ltd.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package uk.ac.diamond.scisoft.analysis;
import java.io.IOException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* To run tests that require Python, the execution environment must have a Python with NumPy installed.
*/
public class PythonHelper {
private static final Logger logger = LoggerFactory.getLogger(PythonHelper.class);
/**
* Set to true to enable python tests
*/
public static final String PYTHON_TEST = "org.dawnsci.python.test";
/**
* Path to Python executable (by default, the python in the current PATH is used)
*/
public static final String PYTHON_EXECUTABLE = "org.dawnsci.python.executable";
/**
* Content of PYTHONPATH environment variable (by default, it is left to inherit the current environment)
*/
public static final String PYTHON_PATH = "org.dawnsci.python.path";
/**
* This is by default false but may be overridden by the {@link #PYTHON_TEST} property
*/
public static boolean enablePythonTests() {
return "true".equalsIgnoreCase(System.getProperty(PYTHON_TEST, "false"));
}
private static String PYTHON_EXE = null;
private static String[] PYTHON_ENV = null;
private static void getDefaultPythonProperties() {
PYTHON_EXE = System.getProperty(PYTHON_EXECUTABLE, "python");
String path = System.getProperty(PYTHON_PATH);
if (path != null) {
PYTHON_ENV = new String[] {"PYTHONPATH=" + path};
}
}
static {
getDefaultPythonProperties();
}
/*
* We are re-implementing a call to python here, we really want to reuse PythonTest#exec or
* SimplePythonRunner#runAndGetOutput from PyDev. Any output on stderr means an error has occurred.
*/
public static String runPythonScript(String scriptContents, boolean failOnAnyOutput) throws Exception {
return runPythonScript(scriptContents, PYTHON_ENV, failOnAnyOutput);
}
private static String runPythonScript(String scriptContents, String[] envp, boolean failOnAnyOutput) throws Exception {
return readAndProcessOutput(failOnAnyOutput, new String[] { PYTHON_EXE, "-c", scriptContents }, envp);
}
// public static String runPythonFile(String file, boolean failOnAnyOutput) throws Exception {
// return runPythonFile(file, null, failOnAnyOutput);
// }
//
// public static String runPythonFile(String file, String[] args, boolean failOnAnyOutput) throws Exception {
// return runPythonFile(file, args, PYTHON_ENV, failOnAnyOutput);
// }
//
public static String runPythonFile(String file, String[] args, String[] envp, boolean failOnAnyOutput) throws Exception {
if (args == null)
args = new String[0];
String[] allArgs = new String[2 + args.length];
allArgs[0] = PYTHON_EXE;
allArgs[1] = file;
for (int i = 0; i < args.length; i++) {
allArgs[i + 2] = args[i];
}
return readAndProcessOutput(failOnAnyOutput, allArgs, envp);
}
// public static PythonRunInfo runPythonScriptBackground(String scriptContents) throws Exception {
// return launch(new String[] { PYTHON_EXE, "-c", scriptContents }, null);
// }
public static PythonRunInfo runPythonFileBackground(String file) throws Exception {
return runPythonFileBackground(file, null);
}
private static PythonRunInfo runPythonFileBackground(String file, String[] args) throws Exception {
return runPythonFileBackground(file, args, PYTHON_ENV);
}
public static PythonRunInfo runPythonFileBackground(String file, String[] args, String[] envp) throws Exception {
if (args == null)
args = new String[0];
String[] allArgs = new String[2 + args.length];
allArgs[0] = PYTHON_EXE;
allArgs[1] = file;
int i = 2;
for (String s : args) {
allArgs[i++] = s;
}
if (PYTHON_ENV != null) {
String[] allEnvs = new String[PYTHON_ENV.length + envp.length];
i = 0;
for (String s : PYTHON_ENV) {
allEnvs[i++] = s;
}
for (String s : envp) {
allEnvs[i++] = s;
}
return launch(allArgs, allEnvs);
}
return launch(allArgs, envp);
}
public static class PythonRunInfo {
ThreadStreamReader std, err;
Process process;
public void terminate() {
process.destroy();
}
public String getStdout(boolean failOnAnyOutput) {
try {
// wait until the process completion.
process.waitFor();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
String stderr = err.getContents();
String stdout = std.getContents();
boolean runSuccess = (!failOnAnyOutput || "".equals(stdout)) && "".equals(stderr);
if(!runSuccess)
throw new AssertionError("Unexpected output on stderr:\n" + stderr + "\nor stdout:\n" + stdout + "\n");
return stdout;
}
public boolean hasTerminated() {
try {
process.exitValue();
return true;
} catch (IllegalThreadStateException e) {
return false;
}
}
}
private static String readAndProcessOutput(boolean failOnAnyOutput, String[] args, String[] envp) throws Exception {
PythonRunInfo pythonRunInfo = launch(args, envp);
return pythonRunInfo.getStdout(failOnAnyOutput);
}
private static PythonRunInfo launch(String[] args, String[] envp) throws IOException {
// Short circuit tests when python isn't available
if (!enablePythonTests())
logger.error("Python not available");
PythonRunInfo pythonRunInfo = new PythonRunInfo();
pythonRunInfo.process = Runtime.getRuntime().exec(args, envp);
// No need to synchronize as we'll waitFor() the process before getting the contents.
pythonRunInfo.std = new ThreadStreamReader(pythonRunInfo.process.getInputStream(), false);
pythonRunInfo.err = new ThreadStreamReader(pythonRunInfo.process.getErrorStream(), false);
pythonRunInfo.std.start();
pythonRunInfo.err.start();
try {
// Allow the python process to start before continuing, this is required when starting a server in the Python side.
// The sleep was originally 1000, but this was too low, resulting in intermittent failures during Jenkins testing (see SCI-1893).
Thread.sleep(2000);
} catch (InterruptedException e) {
}
return pythonRunInfo;
}
/**
* Parse an array output by Python into a String[]
*
* Splits on "', '" so that string cannot occur in input otherwise parse will fail to produce correct results
*
* @param pythonStdout
* output from Python
* @return array of strings, cannot be null, may be [0]
*/
public static String[] parseArray(String pythonStdout) {
// simplistic parsing of print out of array
String[] split = pythonStdout.split("', '");
if (split.length > 0) {
// take off open and closing square bracket
split[0] = split[0].substring(1);
int last = split.length - 1;
split[last] = split[last].substring(0, split[last].length() - 1);
if (last == 0 && split[last].length() == 0) {
// nothing left, array was empty
split = new String[0];
} else {
// take off open and closing quote
split[0] = split[0].substring(1);
split[last] = split[last].substring(0, split[last].length() - 1);
}
}
return split;
}
}