/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.codehaus.jstestrunner; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.StringReader; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.Resource; /** * An execution server executes JavaScript tests represented by a list of urls. */ public class JSTestExecutionServer implements TestResultProducer { private static Logger logger = Logger.getLogger(JSTestExecutionServer.class .getName()); /** * Copy the test runner we have to the test folder and make it available to * multiple invocations of the runner. */ private File testRunnerFile; private final Lock testRunnerFileLock = new ReentrantLock(); /** * The location of the test runner file that the execution will use. */ private String testRunnerFilePath; /** * The formatted command to perform. */ private String commandPattern; /** * The process that was started. */ private Process process; /** * The urls relating to the test. */ private List<URL> urls; /** * The logger attached to the process being run. */ private ProcessLogger processLogger; /** * Make a copy of the js file that will drive the execution engine. Copies * are performed in a synchronised fashion and only if it has not been * copied before. * * @throws IOException * if there is a problem copying the file. */ protected void copyTestRunnerFileIfNotExists() throws IOException { assert testRunnerFilePath != null; String testRunnerFilename = "run-qunit.js"; testRunnerFileLock.lock(); try { testRunnerFile = new File(testRunnerFilePath, testRunnerFilename); if (!testRunnerFile.exists()) { // Create any intermediate folders and create the file. new File(testRunnerFilePath).mkdirs(); testRunnerFile.createNewFile(); // Provide an absolute path to the script that actually runs the // test on the test executor. InputStream is = JSTestExecutionServer.class .getResourceAsStream("/org/codehaus/jstestrunner/" + testRunnerFilename); assert is != null; BufferedInputStream bis = new BufferedInputStream(is); try { BufferedOutputStream bos = new BufferedOutputStream( new FileOutputStream(testRunnerFile)); try { int c; while ((c = bis.read()) != -1) { bos.write(c); } } finally { bos.close(); } } finally { bis.close(); } } } finally { testRunnerFileLock.unlock(); } } /** * Get the command string to use. * * @return the command string as an array of args. * @throws IOException * if the target bootstrap js file that drives the tests cannot * be created. */ protected String[] getCommandArgs() throws IOException { assert commandPattern != null && testRunnerFile != null && urls != null; // Parse the command pattern and break it out into a list of args taking // into consideration anything within double quote chars. List<String> args = new ArrayList<String>(); StringReader s = new StringReader(commandPattern); int c; StringBuilder sb = new StringBuilder(); boolean inDoubleQuotes = false; boolean inSingleQuotes = false; while ((c = s.read()) != -1) { if (c == '"' && !inSingleQuotes) { if (inDoubleQuotes) { args.add(sb.toString()); sb = new StringBuilder(); } inDoubleQuotes = !inDoubleQuotes; } else if (c == '\'' && !inDoubleQuotes) { if (inSingleQuotes) { args.add(sb.toString()); sb = new StringBuilder(); } inSingleQuotes = !inSingleQuotes; } else if (!inDoubleQuotes && !inSingleQuotes) { if (c == ' ') { if (sb.length() > 0) { args.add(sb.toString()); sb = new StringBuilder(); } } else { sb.append((char) c); } } else { // In quotes sb.append((char) c); } } if (sb.length() > 0) { args.add(sb.toString()); } // Put the test runner file into a format for formatting. String testRunnerAbsoluteFilePath = testRunnerFile.getAbsolutePath(); // Convert the list of urls to a command line representation (csv). sb = new StringBuilder(); for (URL url : urls) { if (sb.length() > 0) { sb.append(','); } sb.append(url.toString()); } String testUrls = sb.toString(); // Format each argument. String[] formattedArgs = new String[args.size()]; int i = 0; for (String arg : args) { formattedArgs[i++] = String.format(arg, testRunnerAbsoluteFilePath, testUrls); } if (logger.isLoggable(Level.FINEST)) { logger.log(Level.FINEST, "Args to use: " + Arrays.toString(formattedArgs)); } return formattedArgs; } public String getCommandPattern() { return commandPattern; } public String getTestRunnerFilePath() { return testRunnerFilePath; } public List<URL> getUrls() { return urls; } /** * Tests whether the executor is still running. * * @return true if it is. */ public boolean isAvailable() { if (process != null) { try { process.exitValue(); return false; } catch (IllegalThreadStateException e) { return true; } } else { return false; } } @Resource public void setCommandPattern(String commandPattern) { this.commandPattern = commandPattern; } @Resource public void setTestRunnerFilePath(String testRunnerFilePath) { this.testRunnerFilePath = testRunnerFilePath; } @Resource public void setUrls(List<URL> urls) { this.urls = urls; } /** * Start the execution. * * @throws IOException * if something goes wrong. */ public void start() throws IOException { // Ensure that the bootstrap file is available for execution from the // file system. copyTestRunnerFileIfNotExists(); // Get the command args and execute them, merging STDOUT and STDERR ProcessBuilder builder = new ProcessBuilder(getCommandArgs()); builder.redirectErrorStream(true); try { process = builder.start(); } catch (IOException e) { throw new IOException( "The phantomjs executable cannot be launched from the path or from the value of" + " the org.codehaus.jstestrunner.commandPattern property." + " See http://js-testrunner.codehaus.org/usage.html for instructions." + " Original exception: " + e.toString()); } // Use a ProcessLogger to print all output to System.out processLogger = new ProcessLogger(process); processLogger.start(); } /** * Stop the execution. */ public void stop() { if (process != null) { process.destroy(); // Wait for the process to exit; if it's the last process we want the // ProcessLogger to have a chance to log the exit value, if we are in // FINE level if (logger.isLoggable(Level.FINE) && processLogger.isAlive()) { try { processLogger.join(); } catch (InterruptedException e) { logger.log(Level.WARNING, "Exception waiting for process logger to exit: " + e.toString()); } } } } }