/* * Copyright 2014-present Facebook, 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.facebook.buck.testrunner; import com.facebook.buck.test.selectors.TestSelectorList; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.PrintWriter; // NOPMD can't depend on Guava import java.io.StringWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import org.w3c.dom.Document; import org.w3c.dom.Element; /** Base class for both the JUnit and TestNG runners. */ public abstract class BaseRunner { protected static final String ENCODING = "UTF-8"; // This is to be extended when introducing new information into the result .xml file and allow // consumers of that information to understand whether the testrunner they're using supports the // new features. protected static final String[] RUNNER_CAPABILITIES = {"simple_test_selector"}; protected File outputDirectory; protected List<String> testClassNames; protected long defaultTestTimeoutMillis; protected TestSelectorList testSelectorList; protected boolean isDryRun; protected boolean shouldExplainTestSelectors; public abstract void run() throws Throwable; /** * The test result file is written as XML to avoid introducing a dependency on JSON (see class * overview). */ protected void writeResult(String testClassName, List<TestResult> results) throws IOException, ParserConfigurationException, TransformerException { // XML writer logic taken from: // http://www.genedavis.com/library/xml/java_dom_xml_creation.jsp DocumentBuilder docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); Document doc = docBuilder.newDocument(); doc.setXmlVersion("1.1"); Element root = doc.createElement("testcase"); root.setAttribute("name", testClassName); root.setAttribute("runner_capabilities", getRunnerCapabilities()); doc.appendChild(root); for (TestResult result : results) { Element test = doc.createElement("test"); // suite attribute test.setAttribute("suite", result.testClassName); // name attribute test.setAttribute("name", result.testMethodName); // success attribute boolean isSuccess = result.isSuccess(); test.setAttribute("success", Boolean.toString(isSuccess)); // type attribute test.setAttribute("type", result.type.toString()); // time attribute long runTime = result.runTime; test.setAttribute("time", String.valueOf(runTime)); // Include failure details, if appropriate. Throwable failure = result.failure; if (failure != null) { String message = failure.getMessage(); test.setAttribute("message", message); String stacktrace = stackTraceToString(failure); test.setAttribute("stacktrace", stacktrace); } // stdout, if non-empty. if (result.stdOut != null) { Element stdOutEl = doc.createElement("stdout"); stdOutEl.appendChild(doc.createTextNode(result.stdOut)); test.appendChild(stdOutEl); } // stderr, if non-empty. if (result.stdErr != null) { Element stdErrEl = doc.createElement("stderr"); stdErrEl.appendChild(doc.createTextNode(result.stdErr)); test.appendChild(stdErrEl); } root.appendChild(test); } // Create an XML transformer that pretty-prints with a 2-space indent. // The transformer factory uses a system property to find the class to use. We need to default // to the system default since we have the user's classpath and they may not have everything set // up for the XSLT transform to work. String vendor = System.getProperty("java.vm.vendor"); String factoryClass; if ("IBM Corporation".equals(vendor)) { // Used in the IBM JDK --- from // https://www.ibm.com/support/knowledgecenter/SSYKE2_8.0.0/com.ibm.java.aix.80.doc/user/xml/using_xml.html factoryClass = "com.ibm.xtq.xslt.jaxp.compiler.TransformerFactoryImpl"; } else { // Used in the OpenJDK and the Oracle JDK. factoryClass = "com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl"; } // When we get this far, we're exiting, so no need to reset the property. System.setProperty("javax.xml.transform.TransformerFactory", factoryClass); TransformerFactory transformerFactory = TransformerFactory.newInstance(); Transformer trans = transformerFactory.newTransformer(); trans.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no"); trans.setOutputProperty(OutputKeys.INDENT, "yes"); trans.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2"); // Write the result to a file. String testSelectorSuffix = ""; if (!testSelectorList.isEmpty()) { testSelectorSuffix += ".test_selectors"; } if (isDryRun) { testSelectorSuffix += ".dry_run"; } OutputStream output; if (outputDirectory != null) { File outputFile = new File(outputDirectory, testClassName + testSelectorSuffix + ".xml"); output = new BufferedOutputStream(new FileOutputStream(outputFile)); } else { output = System.out; } StreamResult streamResult = new StreamResult(output); DOMSource source = new DOMSource(doc); trans.transform(source, streamResult); if (outputDirectory != null) { output.close(); } } private static String getRunnerCapabilities() { StringBuilder result = new StringBuilder(); int capsLen = RUNNER_CAPABILITIES.length; for (int i = 0; i < capsLen; i++) { String capability = RUNNER_CAPABILITIES[i]; result.append(capability); if (i != capsLen - 1) { result.append(','); } } return result.toString(); } private String stackTraceToString(Throwable exc) { StringWriter writer = new StringWriter(); exc.printStackTrace(new PrintWriter(writer, /* autoFlush */ true)); // NOPMD no Guava return writer.toString(); } /** * Expected arguments are: * * <ul> * <li>(string) output directory * <li>(long) default timeout in milliseconds (0 for no timeout) * <li>(string) newline separated list of test selectors * <li>(string...) fully-qualified names of test classes * </ul> */ protected void parseArgs(String... args) { File outputDirectory = null; long defaultTestTimeoutMillis = Long.MAX_VALUE; TestSelectorList.Builder testSelectorListBuilder = TestSelectorList.builder(); boolean isDryRun = false; boolean shouldExplainTestSelectors = false; List<String> testClassNames = new ArrayList<>(); for (int i = 0; i < args.length; i++) { switch (args[i]) { case "--default-test-timeout": defaultTestTimeoutMillis = Long.parseLong(args[++i]); break; case "--test-selectors": List<String> rawSelectors = Arrays.asList(args[++i].split("\n")); testSelectorListBuilder.addRawSelectors(rawSelectors); break; case "--simple-test-selector": try { testSelectorListBuilder.addSimpleTestSelector(args[++i]); } catch (IllegalArgumentException e) { System.err.printf("--simple-test-selector takes 2 args: [suite] and [method name]."); System.exit(1); } break; case "--b64-test-selector": try { testSelectorListBuilder.addBase64EncodedTestSelector(args[++i]); } catch (IllegalArgumentException e) { System.err.printf("--b64-test-selector takes 2 args: [suite] and [method name]."); System.exit(1); } break; case "--explain-test-selectors": shouldExplainTestSelectors = true; break; case "--dry-run": isDryRun = true; break; case "--output": outputDirectory = new File(args[++i]); if (!outputDirectory.exists()) { System.err.printf("The output directory did not exist: %s\n", outputDirectory); System.exit(1); } break; default: testClassNames.add(args[i]); } } if (testClassNames.isEmpty()) { System.err.println("Must specify at least one test."); System.exit(1); } this.outputDirectory = outputDirectory; this.defaultTestTimeoutMillis = defaultTestTimeoutMillis; this.isDryRun = isDryRun; this.testClassNames = testClassNames; this.testSelectorList = testSelectorListBuilder.build(); if (!testSelectorList.isEmpty() && !shouldExplainTestSelectors) { // Don't bother class-loading any classes that aren't possible, according to test selectors testClassNames.removeIf(name -> !testSelectorList.possiblyIncludesClassName(name)); } this.shouldExplainTestSelectors = shouldExplainTestSelectors; } protected void runAndExit() { int exitCode; // Run the tests. try { run(); // We're using a successful exit code regardless of test outcome since JUnitRunner // is designed to execute all tests and produce a report of success or failure. We've done // that successfully if we've gotten here. exitCode = 0; } catch (Throwable e) { e.printStackTrace(); // We're using a failed exit code here because something in the test runner crashed. We can't // tell whether there were still tests left to be run, so it's safest if we fail. exitCode = 1; } // Explicitly exit to force the test runner to complete even if tests have sloppily left // behind non-daemon threads that would have otherwise forced the process to wait and // eventually timeout. System.exit(exitCode); } }