/* * Copyright (C) 2011 Everit Kft. (http://everit.org) * * 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 org.everit.osgi.dev.maven; import java.io.BufferedReader; import java.io.Closeable; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStream; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.TimeUnit; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugins.annotations.LifecyclePhase; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.plugins.annotations.ResolutionScope; import org.everit.osgi.dev.dist.util.DistConstants; import org.everit.osgi.dev.dist.util.attach.EOSGiVMManager; import org.everit.osgi.dev.dist.util.configuration.LaunchConfigurationDTO; import org.everit.osgi.dev.dist.util.configuration.schema.EnvironmentType; import org.everit.osgi.dev.dist.util.configuration.schema.UseByType; import org.everit.osgi.dev.maven.configuration.EnvironmentConfiguration; import org.everit.osgi.dev.maven.dto.DistributedEnvironmentData; import org.everit.osgi.dev.maven.util.DaemonStreamRedirector; import org.everit.osgi.dev.maven.util.PluginUtil; import org.everit.osgi.dev.testrunner.TestRunnerConstants; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.xml.sax.SAXException; /** * Runs the integration-tests on OSGi environment. It is necessary to add * <i>org.everit.osgi.dev.testrunner</i> and one of the engines as a dependency to the project to * make this goal work. */ @Mojo(name = "integration-test", defaultPhase = LifecyclePhase.INTEGRATION_TEST, requiresProject = true, requiresDependencyResolution = ResolutionScope.TEST) public class IntegrationTestMojo extends DistMojo { /** * A shutdown hook that stops the started OSGi container. */ private class ShutdownHook extends Thread { private final Process process; private final int shutdownTimeout; private final String uniqueLaunchId; ShutdownHook(final Process process, final String uniqueLaunchId, final int shutdownTimeout) { this.process = process; this.uniqueLaunchId = uniqueLaunchId; this.shutdownTimeout = shutdownTimeout; } @Override public void run() { shutdownProcess(process, uniqueLaunchId, shutdownTimeout, 0); } } /** * The most simple implementation of an output stream that redirects all writings to a writer. */ private static final class SimpleWriterOutputStream extends OutputStream { private final OutputStream outputStream; SimpleWriterOutputStream(final OutputStream outputStream) { this.outputStream = outputStream; } @Override public void close() { } @Override public void write(final int b) throws IOException { outputStream.write(b); } } /** * Struct of test results. */ private static class TestResult { private int error; private int failure; private int skipped; private int tests; private void addToSum(final TestResult testResult) { tests += testResult.tests; error += testResult.error; failure += testResult.failure; skipped += testResult.skipped; } } public static final int DEFAULT_TEST_RUNNING_TIMEOUT = 180000; private static final long LOGGING_INTERVAL = 5000; private static final int MILLISECOND_NUM_IN_SECOND = 1000; private static final String SYSTEM_PROPERTY_PREFIX = "-D"; private static final int TIMEOUT_CHECK_INTERVAL = 10; private static int convertTestSuiteAttributeToInt(final Element element, final String attribute, final File resultFile) throws MojoFailureException { String stringValue = element.getAttribute(attribute); if ("".equals(stringValue.trim())) { throw new MojoFailureException( "Invalid test result file " + resultFile.getAbsolutePath() + ". The attribute " + attribute + " in testSuite is not defined."); } try { return Integer.parseInt(stringValue); } catch (NumberFormatException e) { throw new MojoFailureException( "Invalid test result file " + resultFile.getAbsolutePath() + ". The attribute " + attribute + " is invalid."); } } /** * Whether to log the output of the started test JVMs to the standard output and standard error or * not. */ @Parameter(property = "eosgi.consoleLog", defaultValue = "true") protected boolean consoleLog = true; /** * Skipping the integration tests, only execute the dist goal. */ @Parameter(property = DistConstants.PLUGIN_PROPERTY_DIST_ONLY, defaultValue = "false") protected boolean distOnly = false; /** * The folder where the integration test reports will be placed. Please note that the content of * this folder will be deleted before running the tests. */ @Parameter(property = "eosgi.integration-test.targetFolder", defaultValue = "${project.build.directory}/eosgi/integration-test") protected String integrationTestTargetFolder; /** * Skipping this plugin. */ @Parameter(property = "eosgi.test.skip", defaultValue = "false") protected boolean skipTests = false; private void checkExitCode(final Process process, final String environmentId) throws MojoExecutionException { int exitCode = process.exitValue(); if (exitCode != 0) { throw new MojoExecutionException("Test Process of environment " + environmentId + " finished with exit code " + exitCode); } } private void checkExitError(final File resultFolder, final String environmentId) throws MojoFailureException { File exitErrorFile = new File(resultFolder, TestRunnerConstants.SYSTEM_EXIT_ERROR_FILE_NAME); if (exitErrorFile.exists()) { StringBuilder sb = new StringBuilder(); try (FileInputStream fin = new FileInputStream(exitErrorFile)) { InputStreamReader reader = new InputStreamReader(fin, Charset.defaultCharset()); BufferedReader br = new BufferedReader(reader); String line = br.readLine(); while (line != null) { sb.append(line).append("\n"); line = br.readLine(); } } catch (FileNotFoundException e) { getLog().error("Could not find file " + exitErrorFile.getAbsolutePath(), e); } catch (IOException e) { getLog().error("Error during reading exit error file " + exitErrorFile.getAbsolutePath(), e); } getLog().error( "Error during stopping the JVM of the environment " + environmentId + ". Information can be found at " + exitErrorFile.getAbsolutePath() + ". Content of the file is: \n" + sb.toString()); throw new MojoFailureException( "Could not shut down the JVM of the environment " + environmentId + " in a nice way. For more information, see the content of the file: " + exitErrorFile.getAbsolutePath()); } } private ProcessBuilder createTestProcessBuilder(final String environmentId, final File workingDirFile, final String[] command, final File testResultFolder) { String title = "EOSGi TestProcess - " + environmentId; ProcessBuilder processBuilder = new ProcessBuilder(command); processBuilder.redirectInput(ProcessBuilder.Redirect.INHERIT); processBuilder.redirectError(ProcessBuilder.Redirect.PIPE); processBuilder.redirectInput(ProcessBuilder.Redirect.PIPE); processBuilder.directory(workingDirFile); getLog().info("[" + title + "] Working dir: " + workingDirFile); // Supporting TestRunner 4.x and earlier Map<String, String> envMap = new HashMap<>(System.getenv()); envMap.put("EOSGI_STOP_AFTER_TESTS", Boolean.TRUE.toString()); envMap.put("EOSGI_TEST_RESULT_FOLDER", testResultFolder.getAbsolutePath()); processBuilder.environment().putAll(envMap); getLog().info("[" + title + "] Environment: " + processBuilder.environment()); return processBuilder; } private void defineStandardOutputs(final File stdOutFile, final List<OutputStream> stdOuts) throws MojoExecutionException { FileOutputStream stdOutFileOut; try { stdOutFileOut = new FileOutputStream(stdOutFile); } catch (FileNotFoundException e) { throw new MojoExecutionException("Could not open standard output file for writing", e); } stdOuts.add(stdOutFileOut); if (consoleLog) { stdOuts.add(new SimpleWriterOutputStream(System.out)); } } @Override protected void doExecute() throws MojoExecutionException, MojoFailureException { if (distOnly) { super.doExecute(); return; } if (skipTests) { return; } super.doExecute(); getLog().info("OSGi Integrations tests running started"); File reportFolderFile = initializeReportFolder(); TestResult testResultSum = new TestResult(); List<TestResult> testResults = new ArrayList<>(); for (DistributedEnvironmentData distributedEnvironmentData : distributedEnvironmentDataCollection) { // CS_DISABLE_LINE_LENGTH EnvironmentConfiguration environment = distributedEnvironmentData.getEnvironment(); String environmentId = environment.getId(); File distFolderFile = distributedEnvironmentData.getDistributionFolder(); int shutdownTimeout = environment.getShutdownTimeout(); int timeout = environment.getTestRunningTimeout(); TestResult testResult = runIntegrationTestsOnEnvironment( environmentId, distFolderFile, reportFolderFile, shutdownTimeout, timeout); testResults.add(testResult); testResultSum.addToSum(testResult); } printTestResultSum(testResultSum); throwExceptionsBasedOnTestResultsIfNecesssary(testResultSum); } private Closeable doStreamRedirections(final Process process, final File resultFolder) throws MojoExecutionException { File stdOutFile = new File(resultFolder, "system-out.txt"); File stdErrFile = new File(resultFolder, "system-error.txt"); List<OutputStream> stdOuts = new ArrayList<>(); List<OutputStream> stdErrs = new ArrayList<>(); defineStandardOutputs(stdOutFile, stdOuts); defineStandardOutputs(stdErrFile, stdErrs); final DaemonStreamRedirector deamonFileWriterStreamPoller = new DaemonStreamRedirector(process.getInputStream(), stdOuts.toArray(new OutputStream[0]), getLog()); try { deamonFileWriterStreamPoller.start(); } catch (IOException e) { try { deamonFileWriterStreamPoller.close(); } catch (IOException e1) { e.addSuppressed(e1); } throw new MojoExecutionException("Could not start stream redirector for standard output", e); } final DaemonStreamRedirector deamonStdErrPoller = new DaemonStreamRedirector(process.getErrorStream(), stdErrs.toArray(new OutputStream[0]), getLog()); try { deamonStdErrPoller.start(); } catch (IOException e) { try { deamonFileWriterStreamPoller.close(); } catch (IOException e1) { e.addSuppressed(e1); } try { deamonStdErrPoller.close(); } catch (IOException e1) { e.addSuppressed(e1); } throw new MojoExecutionException("Could not start stream redirector for standard output", e); } return new Closeable() { @Override public void close() throws IOException { IOException thrownE = null; try { deamonFileWriterStreamPoller.close(); } catch (IOException e) { thrownE = e; } try { deamonStdErrPoller.close(); } catch (IOException e) { if (thrownE != null) { thrownE.addSuppressed(e); } else { thrownE = e; } } if (thrownE != null) { throw thrownE; } } }; } private File initializeReportFolder() { File reportFolderFile = new File(integrationTestTargetFolder); getLog().info("Integration test output directory: " + reportFolderFile.getAbsolutePath()); if (reportFolderFile.exists()) { PluginUtil.deleteFolderRecurse(reportFolderFile); } reportFolderFile.mkdirs(); return reportFolderFile; } private void printEnvironmentProcessStartToLog(final String environemntId) { StringBuilder sb = new StringBuilder("\n"); sb.append("-------------------------------------------------------\n"); sb.append("Starting test environment: ").append(environemntId).append("\n"); sb.append("-------------------------------------------------------\n\n"); getLog().info(sb.toString()); } private void printTestResultsOfEnvironment(final String environmentId, final TestResult testResult) { StringBuilder sb = new StringBuilder("\n"); sb.append("-------------------------------------------------------\n"); sb.append("Test environment finished: ").append(environmentId).append("\n"); sb.append("-------------------------------------------------------\n\n"); sb.append("Results:\n\n"); sb.append("Tests run: ").append(testResult.tests); sb.append(", Failures: ").append(testResult.failure); sb.append(", Errors: ").append(testResult.error); sb.append(", Skipped: ").append(testResult.skipped); getLog().info(sb.toString()); } private void printTestResultSum(final TestResult testResultSum) { StringBuilder sb = new StringBuilder(); sb.append("\n"); sb.append("-------------------------------------------------------\n"); sb.append("I N T E G R A T I O N T E S T S ( O S G i)\n"); sb.append("-------------------------------------------------------\n\n"); sb.append("Results:\n\n"); sb.append("Tests run: ").append(testResultSum.tests); sb.append(", Failures: ").append(testResultSum.failure); sb.append(", Errors: ").append(testResultSum.error); sb.append(", Skipped: ").append(testResultSum.skipped); sb.append("\n"); getLog().info(sb.toString()); } private void processResults(final File testResultFolder, final TestResult results) throws MojoFailureException { DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); DocumentBuilder documentBuilder = null; try { documentBuilder = documentBuilderFactory.newDocumentBuilder(); } catch (ParserConfigurationException e) { throw new MojoFailureException("Failed to process test results", e); } boolean foundTestResult = false; if (testResultFolder.exists() && testResultFolder.isDirectory()) { File[] files = testResultFolder.listFiles(); for (File resultFile : files) { if (resultFile.getName().endsWith(".xml")) { processResultXML(results, documentBuilder, resultFile); foundTestResult = true; } } } if (!foundTestResult) { throw new MojoFailureException( "No test result found in folder: " + testResultFolder.toString()); } } private void processResultXML(final TestResult results, final DocumentBuilder documentBuilder, final File resultFile) throws MojoFailureException { try (FileInputStream fin = new FileInputStream(resultFile)) { Document document = documentBuilder.parse(fin); Element testSuite = document.getDocumentElement(); if (!"testsuite".equals(testSuite.getNodeName())) { throw new MojoFailureException("Invalid test result xml file " + resultFile.getAbsolutePath() + ". Root element is not testsuite."); } results.tests += IntegrationTestMojo.convertTestSuiteAttributeToInt(testSuite, "tests", resultFile); results.failure += IntegrationTestMojo.convertTestSuiteAttributeToInt(testSuite, "failures", resultFile); results.error += IntegrationTestMojo.convertTestSuiteAttributeToInt(testSuite, "errors", resultFile); results.skipped += IntegrationTestMojo.convertTestSuiteAttributeToInt(testSuite, "skipped", resultFile); } catch (SAXException e) { throw new MojoFailureException( "Invalid test result file " + resultFile.getAbsolutePath()); } catch (IOException e) { throw new MojoFailureException("Error during processing result file " + resultFile.getAbsolutePath()); } } private String[] resolveCommandForEnvironment(final File distFolderFile, final File testResultFolder, final String uniqueLaunchId) throws MojoFailureException { EnvironmentType distributedEnvironment = distEnvConfigProvider.getOverriddenDistributedEnvironmentConfig( new File(distFolderFile, DistConstants.FILE_NAME_EOSGI_DIST_CONFIG), UseByType.INTEGRATION_TEST); LaunchConfigurationDTO environmentConfigurationDTO = distEnvConfigProvider.getLaunchConfiguration(distributedEnvironment); List<String> command = new ArrayList<>(); command.add(PluginUtil.getJavaCommand()); String classPath = environmentConfigurationDTO.classpath; if ((classPath != null) && !classPath.trim().isEmpty()) { command.add("-classpath"); command.add(classPath); } command.addAll(environmentConfigurationDTO.vmArguments); command.add( SYSTEM_PROPERTY_PREFIX + DistConstants.SYSPROP_LAUNCH_UNIQUE_ID + "=" + uniqueLaunchId); // Supporting TestRunner 5.x command.add(SYSTEM_PROPERTY_PREFIX + TestRunnerConstants.PROP_DEVELOPMENT_MODE + "=false"); command.add(SYSTEM_PROPERTY_PREFIX + TestRunnerConstants.PROP_STOP_AFTER_TESTS + "=true"); command.add("-D" + TestRunnerConstants.PROP_TEST_RESULT_FOLDER + '=' + testResultFolder.getAbsolutePath()); command.add(environmentConfigurationDTO.mainClass); command.addAll(environmentConfigurationDTO.programArguments); return command.toArray(new String[] {}); } private TestResult runIntegrationTestsOnEnvironment(final String environmentId, final File distFolderFile, final File reportFolderFile, final int shutdownTimeout, final int testRunTimeout) throws MojoFailureException, MojoExecutionException { printEnvironmentProcessStartToLog(environmentId); TestResult testResult = new TestResult(); File testResultFolder = PluginUtil.subFolderFile(reportFolderFile, environmentId, "test-result"); testResultFolder.mkdirs(); String uniqueLaunchId = UUID.randomUUID().toString(); String[] command = resolveCommandForEnvironment(distFolderFile, testResultFolder, uniqueLaunchId); try { ProcessBuilder processBuilder = createTestProcessBuilder( environmentId, distFolderFile, command, testResultFolder); Process process = processBuilder.start(); boolean timeoutHappened = false; File outputFolderFile = PluginUtil.subFolderFile(reportFolderFile, environmentId, "console-output"); outputFolderFile.mkdirs(); ShutdownHook shutdownHook = new ShutdownHook(process, uniqueLaunchId, shutdownTimeout); Runtime runtime = Runtime.getRuntime(); runtime.addShutdownHook(shutdownHook); try (Closeable redirectionCloseable = doStreamRedirections(process, outputFolderFile)) { waitForProcess(process, testRunTimeout); if (process.isAlive()) { getLog().warn("Test running process did not stop until timeout. Forcing to stop it..."); timeoutHappened = true; shutdownProcess(process, uniqueLaunchId, shutdownTimeout, -1); } } finally { runtime.removeShutdownHook(shutdownHook); } if (timeoutHappened) { throw new MojoExecutionException("Test process of environment " + "[" + environmentId + "] did not finish within timeout"); } checkExitError(testResultFolder, environmentId); checkExitCode(process, environmentId); getLog().info("Analyzing test results..."); processResults(testResultFolder, testResult); printTestResultsOfEnvironment(environmentId, testResult); } catch (IOException e) { throw new MojoExecutionException("Error during running integration tests", e); } return testResult; } private void shutdownProcess(final Process process, final String uniqueLaunchId, final int shutdownTimeout, final int code) { getLog().warn("Stopping test process: " + process); if (!process.isAlive()) { return; } try (EOSGiVMManager vmManager = createEOSGiVMManager()) { String virtualMachineId = vmManager.getVirtualMachineIdByIUniqueLaunchId(uniqueLaunchId); if (virtualMachineId != null) { vmManager.shutDownVirtualMachine(virtualMachineId, code, null); } } catch (Exception e) { getLog().error("Could not stop VM via Attach. Shutting it down forcibly", e); process.destroyForcibly(); return; } try { process.waitFor(shutdownTimeout, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { // Do nothing } if (process.isAlive()) { process.destroyForcibly(); } } private void throwExceptionsBasedOnTestResultsIfNecesssary(final TestResult resultSum) throws MojoFailureException { if ((resultSum.error > 0) || (resultSum.failure > 0)) { throw new MojoFailureException("Error during running OSGi integration tests"); } } private void waitForProcess(final Process process, final long timeout) { long startTime = System.currentTimeMillis(); long nextExpectedLogging = startTime + LOGGING_INTERVAL; long latestEndTime = startTime + timeout; long currentTime = startTime; while (process.isAlive() && (currentTime < latestEndTime)) { try { Thread.sleep(TIMEOUT_CHECK_INTERVAL); } catch (InterruptedException e) { Thread.currentThread().interrupt(); getLog().info("Waiting for tests was interrupted."); return; } if (!consoleLog && (currentTime > nextExpectedLogging)) { long secondsSinceStart = (nextExpectedLogging - startTime) / MILLISECOND_NUM_IN_SECOND; getLog().info("Waiting for test results since " + secondsSinceStart + "s"); nextExpectedLogging = nextExpectedLogging + LOGGING_INTERVAL; } currentTime = System.currentTimeMillis(); } } }