/* * Copyright 2009 Google Inc. * * Licensed 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 com.google.jstestdriver.idea; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.inject.AbstractModule; import com.google.inject.Module; import com.google.inject.multibindings.Multibinder; import com.google.inject.name.Names; import com.google.jstestdriver.*; import com.google.jstestdriver.config.Configuration; import com.google.jstestdriver.config.ConfigurationSource; import com.google.jstestdriver.config.UserConfigurationSource; import com.google.jstestdriver.config.YamlParser; import com.google.jstestdriver.hooks.FileParsePostProcessor; import com.google.jstestdriver.idea.execution.tree.JstdTestRunnerFailure; import com.google.jstestdriver.idea.server.JstdServerFetchResult; import com.google.jstestdriver.idea.server.JstdServerUtils; import com.google.jstestdriver.idea.util.EnumUtils; import com.google.jstestdriver.idea.util.ObjectUtils; import com.google.jstestdriver.output.MultiTestResultListener; import com.google.jstestdriver.output.TestResultHolder; import com.google.jstestdriver.output.TestResultListener; import com.google.jstestdriver.runner.RunnerMode; import com.google.jstestdriver.util.DisplayPathSanitizer; import org.jetbrains.annotations.NotNull; import java.io.*; import java.net.*; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.logging.LogManager; import java.util.regex.Pattern; import static com.google.inject.multibindings.Multibinder.newSetBinder; /** * Run JSTD in its own process, and stream messages via a socket to a server that lives in the IDEA process, * which will update the UI with our results. * * @author alexeagle@google.com (Alex Eagle) */ public class TestRunner { public enum ParameterKey { PORT, SERVER_URL, CONFIG_FILE, TEST_CASE, TEST_METHOD } private final Settings mySettings; private final ObjectOutput myTestResultProtocolMessageOutput; public TestRunner(Settings settings, ObjectOutput testResultProtocolMessageOutput) { mySettings = settings; myTestResultProtocolMessageOutput = testResultProtocolMessageOutput; } public void execute() throws InterruptedException { for (File config : mySettings.getConfigFiles()) { try { execute(config); } catch (Exception e) { String message = formatMessage(null, e); JstdTestRunnerFailure failure = new JstdTestRunnerFailure(JstdTestRunnerFailure.FailureType.SINGLE_JSTD_CONFIG, message, config); try { myTestResultProtocolMessageOutput.writeObject(failure); } catch (IOException ioe) { ioe.printStackTrace(); } } } } private void execute(File config) { final String testCaseName; if (!mySettings.getTestCaseName().isEmpty()) { if (!mySettings.getTestMethodName().isEmpty()) { testCaseName = mySettings.getTestCaseName() + "." + mySettings.getTestMethodName(); } else { testCaseName = mySettings.getTestCaseName(); } } else { testCaseName = "all"; } List<String> testCaseNames = Collections.singletonList(testCaseName); final ActionRunner dryRunRunner = makeActionBuilder(config).dryRunFor(testCaseNames).build(); final ActionRunner testRunner = makeActionBuilder(config).addTests(testCaseNames).build(); //TODO(alexeagle): support client-side reset action final ActionRunner resetRunner = makeActionBuilder(config).resetBrowsers().build(); dryRunRunner.runActions(); testRunner.runActions(); resetRunner.runActions(); } private IDEPluginActionBuilder makeActionBuilder(File configFile) { FlagsImpl flags = new FlagsImpl(); flags.setServer(mySettings.getServerUrl()); flags.setCaptureConsole(true); Configuration configuration = resolveConfiguration(configFile, flags); IDEPluginActionBuilder builder = new IDEPluginActionBuilder(configuration, flags); List<Module> modules = new PluginLoader().load(configuration.getPlugins()); for (Module module : modules) { builder.install(module); } builder.install(createTestResultPrintingModule(configFile)); return builder; } private Configuration resolveConfiguration(File configFile, FlagsImpl flags) { try { ConfigurationSource confSrc = new UserConfigurationSource(configFile); File initialBasePath = configFile.getParentFile(); Configuration parsedConf = confSrc.parse(initialBasePath, new YamlParser()); File resolvedBasePath = parsedConf.getBasePath().getCanonicalFile(); PathResolver pathResolver = new PathResolver( resolvedBasePath, Collections.<FileParsePostProcessor>emptySet(), new DisplayPathSanitizer(resolvedBasePath) ); return parsedConf.resolvePaths(pathResolver, flags); } catch (Exception e) { throw new RuntimeException("Failed to read settings file " + configFile, e); } } private Module createTestResultPrintingModule(final File configFile) { return new AbstractModule() { @Override protected void configure() { bind(ObjectOutput.class).annotatedWith(Names.named("testResultProtocolMessageOutput")) .toInstance(myTestResultProtocolMessageOutput); bind(File.class).annotatedWith(Names.named("jstdConfigFile")).toInstance(configFile); Multibinder<TestResultListener> testResultListeners = newSetBinder(binder(), TestResultListener.class); testResultListeners.addBinding().to(TestResultHolder.class); bind(TestResultListener.class).to(MultiTestResultListener.class); newSetBinder(binder(), ResponseStreamFactory.class).addBinding().to(TestRunnerResponseStreamFactory.class); } }; } private static String formatMessage(String message, Throwable t) { StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); t.printStackTrace(pw); pw.close(); if (message == null) { return sw.toString(); } else { return message + "\n\n" + sw.toString(); } } public static void main(String[] args) throws Exception { // System.out.println(Arrays.toString(args)); LogManager.getLogManager().readConfiguration(RunnerMode.QUIET.getLogConfig()); Map<ParameterKey, String> paramMap = parseParams(args); Settings settings = Settings.build(paramMap); ObjectOutput testResultProtocolMessageOutput = fetchSocketObjectOutput(settings.getPort()); if (!validateServer(testResultProtocolMessageOutput, paramMap)) { return; } try { new TestRunner(settings, testResultProtocolMessageOutput).execute(); } catch (Exception ex) { String message = formatMessage("JsTestDriver crashed!", ex); testResultProtocolMessageOutput.writeObject(new JstdTestRunnerFailure(JstdTestRunnerFailure.FailureType.WHOLE_TEST_RUNNER, message, null)); } finally { try { testResultProtocolMessageOutput.close(); } catch (Exception e) { System.err.println("Exception occurred while closing testResultProtocolMessageOutput"); e.printStackTrace(); } } } static boolean validateServer(ObjectOutput testResultProtocolMessageOutput, Map<ParameterKey, String> paramMap) throws IOException { String serverUrl = paramMap.get(ParameterKey.SERVER_URL); if (serverUrl != null && !serverUrl.isEmpty()) { JstdServerFetchResult fetchResult = JstdServerUtils.syncFetchServerInfo(serverUrl); String message = null; if (fetchResult.isError()) { message = "Could not connect to a JsTestDriver server running at " + serverUrl + "\n" + "Check that the server is running."; } else if (fetchResult.getServerInfo().getCapturedBrowsers().isEmpty()) { message = "No captured browsers found.\n" + "To capture browser open '" + serverUrl + "' in browser."; } if (message != null) { testResultProtocolMessageOutput.writeObject(new JstdTestRunnerFailure(JstdTestRunnerFailure.FailureType.WHOLE_TEST_RUNNER, message, null)); return false; } } return true; } private static Map<ParameterKey, String> parseParams(String[] args) { Map<ParameterKey, String> params = Maps.newHashMap(); for (String arg : args) { int delimiterIndex = arg.indexOf('='); if (delimiterIndex != -1) { String key = arg.substring(0, delimiterIndex); String value = arg.substring(delimiterIndex + 1, arg.length()); if (key.startsWith("--")) { key = key.substring(2); ParameterKey parameterKey = EnumUtils.findEnum(ParameterKey.class, key, false); if (parameterKey != null) { params.put(parameterKey, value); } } } } return params; } @NotNull private static ObjectOutput fetchSocketObjectOutput(int port) { try { SocketAddress endpoint = new InetSocketAddress(InetAddress.getByName(null), port); final Socket socket = connectToServer(endpoint, 2 * 1000, 5); try { return new ObjectOutputStream(socket.getOutputStream()) { @Override public void close() throws IOException { socket.close(); // socket's input and output streams are closed too } }; } catch (IOException inner) { closeSocketSilently(socket); throw inner; } } catch (IOException e) { throw new RuntimeException("Could not connect to IDE, address: " + "'localhost:" + port + "'", e); } } private static Socket connectToServer(SocketAddress endpoint, int connectTimeoutMillis, int retries) throws IOException { IOException saved = null; for (int i = 0; i < retries; i++) { Socket socket = new Socket(); try { socket.connect(endpoint, connectTimeoutMillis); return socket; } catch (IOException e) { closeSocketSilently(socket); saved = e; } } throw saved; } private static void closeSocketSilently(Socket socket) { try { socket.close(); } catch (Exception e) { // swallow exception } } private static class Settings { private final int myPort; private final String myServerUrl; private final List<File> myConfigFiles; private final String myTestCaseName; private final String myTestMethodName; private Settings(int port, String serverUrl, List<File> configFiles, String testCaseName, String testMethodName) { myPort = port; myServerUrl = serverUrl; myConfigFiles = configFiles; myTestCaseName = testCaseName; myTestMethodName = testMethodName; } public int getPort() { return myPort; } @NotNull public String getServerUrl() { return myServerUrl; } public List<File> getConfigFiles() { return myConfigFiles; } public String getTestCaseName() { return myTestCaseName; } public String getTestMethodName() { return myTestMethodName; } @NotNull private static Settings build(Map<ParameterKey, String> parameters) { int port = Integer.parseInt(parameters.get(ParameterKey.PORT)); String serverUrl = parameters.get(ParameterKey.SERVER_URL); if (serverUrl == null) { throw new RuntimeException("server_url parameter must be specified"); } String configFilesStr = ObjectUtils.notNull(parameters.get(ParameterKey.CONFIG_FILE), ""); String[] pathes = configFilesStr.split(Pattern.quote(",")); List<File> configFiles = Lists.newArrayList(); for (String urlEncodedPath : pathes) { try { String path = URLDecoder.decode(urlEncodedPath, "UTF-8"); File file = new File(path); if (file.isFile()) { configFiles.add(file); } } catch (UnsupportedEncodingException ignored) {} } if (configFiles.isEmpty()) { throw new RuntimeException("No valid config files found"); } String testCaseName = ObjectUtils.notNull(parameters.get(ParameterKey.TEST_CASE), ""); String testMethodName = ObjectUtils.notNull(parameters.get(ParameterKey.TEST_METHOD), ""); return new Settings(port, serverUrl, configFiles, testCaseName, testMethodName); } } }