package com.occamlab.te.spi.executors.testng; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.URI; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.UUID; import java.util.logging.Level; import java.util.logging.Logger; import javax.xml.transform.Source; import javax.xml.transform.sax.SAXSource; import org.testng.TestNG; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; import org.xml.sax.InputSource; import com.occamlab.te.spi.executors.TestRunExecutor; /** * * Executes a TestNG test suite using the given test run arguments. */ public class TestNGExecutor implements TestRunExecutor { private static final Logger LOGR = Logger.getLogger(TestNGExecutor.class.getPackage().getName()); private boolean useDefaultListeners; private File outputDir; private URI testngConfig; /** * Constructs a TestNG executor with the given test suite definition. The * default listeners are <strong>not</strong> used. * * @param testngSuite * A reference to a file containing a TestNG suite definition * (with <suite> as the document element). */ public TestNGExecutor(String testngSuite) { this(testngSuite, System.getProperty("java.io.tmpdir"), false); } /** * Constructs a TestNG executor configured as indicated. * * @param testngSuite * A reference to a file containing a TestNG suite definition. * @param outputDirPath * The location of the root directory for writing test results. * If the directory does not exist and cannot be created, the * location given by the "java.io.tmpdir" system property is used * instead. * @param useDefaultListeners * A boolean value indicating whether or not to use the default * set of listeners. */ public TestNGExecutor(String testngSuite, String outputDirPath, boolean useDefaultListeners) { this.useDefaultListeners = useDefaultListeners; this.outputDir = new File(outputDirPath, "testng"); if (!this.outputDir.exists() && !this.outputDir.mkdirs()) { LOGR.config("Failed to create output directory at " + this.outputDir); this.outputDir = new File(System.getProperty("java.io.tmpdir")); } if (null != testngSuite && !testngSuite.isEmpty()) { this.testngConfig = URI.create(testngSuite); } } /** * Executes a test suite using the supplied test run arguments. The test run * arguments are expected to be contained in an XML properties document * structured as shown in the following example. * * <pre> * {@code * <!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd"> * <properties version="1.0"> * <comment>Test run arguments</comment> * <entry key="arg1">atom-feed.xml</entry> * <entry key="arg2">L2</entry> * </properties> * } * </pre> * * <p> * <strong>Note:</strong>The actual arguments (key-value pairs) are * suite-specific. * </p> * * @param testRunArgs * A DOM Document node that contains a set of XML properties. * @return A Source object that provides an XML representation of the test * results. */ @Override public Source execute(Document testRunArgs) { if (null == testRunArgs) { throw new IllegalArgumentException("No test run arguments were supplied."); } TestNG driver = new TestNG(); setTestSuites(driver, this.testngConfig); driver.setVerbose(0); driver.setUseDefaultListeners(this.useDefaultListeners); UUID runId = UUID.randomUUID(); File runDir = new File(this.outputDir, runId.toString()); if (!runDir.mkdir()) { runDir = this.outputDir; LOGR.config("Created test run directory at " + runDir.getAbsolutePath()); } driver.setOutputDirectory(runDir.getAbsolutePath()); AlterSuiteParametersListener listener = new AlterSuiteParametersListener(); listener.setTestRunArgs(testRunArgs); listener.setTestRunId(runId); driver.addAlterSuiteListener(listener); driver.run(); Source source = null; try { File resultsFile = getResultsFile(getPreferredMediaType(testRunArgs), driver.getOutputDirectory()); InputStream inStream = new FileInputStream(resultsFile); InputSource inSource = new InputSource(new InputStreamReader(inStream, StandardCharsets.UTF_8)); source = new SAXSource(inSource); source.setSystemId(resultsFile.toURI().toString()); } catch (IOException e) { LOGR.log(Level.SEVERE, "Error reading test results: " + e.getMessage()); } return source; } /** * Returns the test results in the specified format. The default media type * is "application/xml", but "application/rdf+xml" (RDF/XML) is also * supported. * * @param mediaType * The media type of the test results (XML or RDF/XML). * @param outputDirectory * The directory containing the test run output. * @return A File containing the test results. * @throws FileNotFoundException * If no test results are found. */ File getResultsFile(String mediaType, String outputDirectory) throws FileNotFoundException { // split out any media type parameters String contentType = mediaType.split(";")[0]; String fileName = (contentType.endsWith("rdf+xml")) ? "earl.rdf" : "testng-results.xml"; File resultsFile = new File(outputDirectory, fileName); if (!resultsFile.exists()) { throw new FileNotFoundException("Test run results not found at " + resultsFile.getAbsolutePath()); } return resultsFile; } /** * Gets the preferred media type for the test results as indicated by the * value of the "acceptMediaType" key in the given properties file. The * default value is "application/xml". * * @param testRunArgs * An XML properties file containing test run arguments. * @return The preferred media type. */ String getPreferredMediaType(Document testRunArgs) { String mediaType = "application/xml"; NodeList entries = testRunArgs.getElementsByTagName("entry"); for (int i = 0; i < entries.getLength(); i++) { Element entry = (Element) entries.item(i); if (entry.getAttribute("key").equals("acceptMediaType")) { mediaType = entry.getTextContent().trim(); } } return mediaType; } /** * Sets the test suite to run using the given URI reference. Three types of * references are supported: * <ul> * <li>A file system reference</li> * <li>A file: URI</li> * <li>A jar: URI</li> * </ul> * * @param driver * The main TestNG driver. * @param ets * A URI referring to a suite definition. */ private void setTestSuites(TestNG driver, URI ets) { if (ets.getScheme().equalsIgnoreCase("jar")) { // jar:{url}!/{entry} String[] jarPath = ets.getSchemeSpecificPart().split("!"); File jarFile = new File(URI.create(jarPath[0])); driver.setTestJar(jarFile.getAbsolutePath()); driver.setXmlPathInJar(jarPath[1].substring(1)); } else { List<String> testSuites = new ArrayList<String>(); File tngFile = new File(ets); if (tngFile.exists()) { LOGR.log(Level.CONFIG, "Using TestNG config file {0}", tngFile.getAbsolutePath()); testSuites.add(tngFile.getAbsolutePath()); } else { throw new IllegalArgumentException("A valid TestNG config file reference is required."); } driver.setTestSuites(testSuites); } } }