de.fischer.thotti.core.runner.NDRunner.java Source code

Java tutorial

Introduction

Here is the source code for de.fischer.thotti.core.runner.NDRunner.java

Source

/*
 * Copyright 2011 Oliver B. Fischer
 *
 * 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 de.fischer.thotti.core.runner;

import de.fischer.thotti.core.misc.StopWatch;
import de.fischer.thotti.core.resources.ndresult.NonDistributedTestResultType;
import de.fischer.thotti.core.resources.ndresult.TestSuiteResult;
import de.fischer.thotti.core.resources.ndtest.NonDistributedTestType;
import de.fischer.thotti.core.resources.ndtest.NonDistributedTestsType;
import de.fischer.thotti.core.resources.ndtest.TestInstanceType;
import de.fischer.thotti.core.resources.ndtest.TestSuite;
import de.fischer.thotti.ec2.core.ThottiRuntimeException;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.GnuParser;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.OptionBuilder;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.commons.exec.CommandLine;
import org.apache.commons.exec.DefaultExecutor;
import org.apache.commons.exec.ExecuteException;
import org.apache.commons.lang3.StringUtils;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.datatype.DatatypeConfigurationException;
import javax.xml.datatype.DatatypeFactory;
import javax.xml.datatype.Duration;
import javax.xml.datatype.XMLGregorianCalendar;
import java.io.File;
import java.io.IOException;
import java.math.BigInteger;
import java.net.InetAddress;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;

/**
 * The nondistributed test runner executes a given test configuration.
 * <p/>
 * <h2>Implementation Notes</h2>
 *
 * @author Oliver Fischer
 */
public class NDRunner {

    /**
     * Return code of the {@link NDRunner} if the execution was successfull
     * without any internal errors. This return code indicates only the
     * successfull execution of the runner itself.
     */
    public static final int SUCCESS = 0;

    /**
     * Return code of the {@link NDRunner} if the execution was not
     * successfull. This return code indicates an internal error
     * like a misconfiguration.
     */
    public static final int ERROR = 1;

    /**
     * Return code of the {@link NDRunner} in slave mode if
     * a test run failed because the test class was't found.
     */
    public static final int TEST_CLASS_NOT_FOUND = 5;

    /**
     * Return code of the {@link NDRunner} in slave mode if
     * a test run failed because the test method wasn't found.
     */
    public static final int TEST_METHOD_NOT_FOUND = 6;

    /**
     * Return code of the {@link NDRunner} in slave mode if
     * the test method is overloaded and the name
     * of the test method is not unique.
     */
    public static final int TEST_METHOD_AMBIGOUS = 7;

    /**
     * Return code of the {@link NDRunner} in slave mode if
     * the signature of the test method is not compatible
     * with the parameter specification in the test configuration.
     */
    public static final int TEST_METHOD_SIGNATURE_MISMATCH = 8;

    /**
     * The file name of the standard Thotti configuration file.
     */
    public final String THOTTI_STANDARD_TEST_CONFIGURATION = "thotti-test-config.xml";

    protected StopWatch stopWatch = new StopWatch();

    protected URL configResource;

    protected TestSuite testSuite;

    public static final String THOTTI_RESULT_FILE = "thotti-result.xml";

    /**
     * Main method of the {@link NDRunner}.
     */
    public static void main(String[] args) throws IOException {
        try {
            // @todo Evaluate --jopt-simple
            Options options = createCommandLineOption();

            CommandLineParser parser = new GnuParser();
            try {
                // parse the command line arguments
                org.apache.commons.cli.CommandLine line = parser.parse(options, args);

                if (line.hasOption("slave")) {
                    if (!line.hasOption("executionid")) {
                        throw new ParseException("Execution id is missing.");
                    }

                    String eid = line.getOptionValue("executionid");

                    int eidInt = Integer.valueOf(eid);

                    new NDRunner().runSlaveMode(eidInt);
                } else {
                    new NDRunner().runMasterMode();
                }
            } catch (ParseException exp) {
                System.err.println("Parsing failed.  Reason: " + exp.getMessage());
            }
        } catch (RunnerException e) {
            if (e.getCause() != null) {
                e.printStackTrace(System.err);
                System.err.println();
            }

            System.err.println(e.getMessage());
            System.exit(ERROR);
        }

    }

    private static Options createCommandLineOption() {
        Options options = new Options();

        Option version = new Option("slave", "Run in slave mode");

        Option executionid = OptionBuilder.withArgName("id").hasArg()
                .withDescription("Run the test with this execution id.").create("executionid");

        options.addOption(version);
        options.addOption(executionid);

        return options;
    }

    /**
     * Starts the test runner in slave mode.
     * <p/>
     * In the slave mode the runner will only execute
     * the test instance with the given execution ID.
     *
     * @param executionID The execution id of the test
     *                    to execute.
     * @see #runMasterMode()
     */
    private void runSlaveMode(int executionID) throws RunnerException, IOException {
        configResource = findTestConfiguration();
        testSuite = loadTestConfiguration();

        FindResult found = findInstanceByID(testSuite, executionID);

        executeSingleTest(found.test, found.instance);
    }

    protected FindResult findInstanceByID(TestSuite testSuite, int executionID) {
        if (testSuite == null)
            throw new NullPointerException();

        // Remember, the execution id is unique within a given test suite
        // but only for he given suite. Generating the same tests suite
        // from the same annotations might result in different IDs
        TestInstanceType instance = null;
        NonDistributedTestType testOfInstance = null;

        NonDistributedTestsType tests = testSuite.getNonDistributedTests();

        for (NonDistributedTestType test : tests.getNonDistributedTests()) {
            for (TestInstanceType current : test.getTestInstances()) {
                BigInteger id = current.getExecutionID();

                if (id.intValue() == executionID) {
                    instance = current;
                    testOfInstance = test;

                    break;
                }
            }
        }

        if (instance == null) {
            throw new ThottiRuntimeException(
                    "Internal error while running in " + "slave mode: No test instace " + executionID + " found.");
        }

        return new FindResult(testOfInstance, instance);
    }

    /**
     * Starts the test runner in the master mode.
     */
    private void runMasterMode() throws RunnerException, IOException {
        configResource = findTestConfiguration();
        testSuite = loadTestConfiguration();

        assert testSuite.getNonDistributedTests() != null;

        List<NDTestResult> resultList = new LinkedList<NDTestResult>();
        NonDistributedTestsType ndTest = testSuite.getNonDistributedTests();
        List<NonDistributedTestType> tests = ndTest.getNonDistributedTests();

        for (NonDistributedTestType test : tests) {
            for (TestInstanceType instance : test.getTestInstances()) {
                NDTestResult result = forkNDTestRunnerInSlaveMode(test, instance);

                resultList.add(result);
            }
        }

        TestSuiteResult resultType = generatedTestResult(resultList);
        saveTestResult(resultType);
    }

    private TestSuiteResult generatedTestResult(List<NDTestResult> resultList) throws RunnerException {
        if (null == resultList) {
            throw new NullPointerException();
        }

        DatatypeFactory datatypeFac = null;

        try {
            datatypeFac = DatatypeFactory.newInstance();
        } catch (DatatypeConfigurationException e) {
            throw new RunnerException("Failed to instanciate datatype factory.", e);
        }

        de.fischer.thotti.core.resources.ndtest.ObjectFactory factory = new de.fischer.thotti.core.resources.ndtest.ObjectFactory();

        de.fischer.thotti.core.resources.ndresult.ObjectFactory factory2 = new de.fischer.thotti.core.resources.ndresult.ObjectFactory();

        TestSuiteResult suiteResult = factory2.createTestSuiteResult();

        for (NDTestResult result : resultList) {
            NonDistributedTestResultType ndResult = factory2.createNonDistributedTestResultType();

            Duration execTime = datatypeFac.newDuration(result.getExecutionTime());
            XMLGregorianCalendar startTime = datatypeFac.newXMLGregorianCalendar(result.getStartTime());
            ndResult.setTestID(result.getTestId());
            ndResult.setExitCode(BigInteger.valueOf(result.getExitCode()));
            ndResult.setJvmArgsID(result.getJVMArgsID());
            ndResult.setJvmArgs(result.getJvmArgs());
            ndResult.setExecutionTimeMS(execTime);
            ndResult.setStartedAt(startTime);
            ndResult.setParamGrpID(result.getParamGrpID());

            try {
                ndResult.setMahoutVersion(org.apache.mahout.Version.versionFromResource());
            } catch (IOException e) {
                e.printStackTrace(); //To change body of catch statement use File | Settings | File Templates.
            }
            suiteResult.getNonDistributedTestResults().add(ndResult);
        }

        suiteResult.setUuid(generateResultUUID());
        return suiteResult;
    }

    protected void saveTestResult(TestSuiteResult result) {
        Package pkg = TestSuiteResult.class.getPackage();

        File resultFile = generateResultFileName();

        JAXBContext jaxbContext = null;

        try {
            jaxbContext = JAXBContext.newInstance(pkg.getName());

            Marshaller marshaller = jaxbContext.createMarshaller();
            marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);

            marshaller.marshal(result, resultFile);
        } catch (JAXBException e) {
            // @todo?
        }
    }

    private NDTestResult forkNDTestRunnerInSlaveMode(NonDistributedTestType test, TestInstanceType instance)
            throws IOException {
        NDTestResult result = new NDTestResult();
        GregorianCalendar startTime;
        long startTimeMS;
        long endTimeMS;
        int exitCode = -9999;

        result.setTestId(test.getId());
        result.setJVMArgs(instance.getJvmArguments());
        result.setJVMArgsID(instance.getJvmArgsID());
        result.setParamGrpID(instance.getName());

        CommandLine cmdLine = new CommandLine("java");

        if (false == StringUtils.isEmpty(instance.getJvmArguments())) {
            cmdLine.addArguments(instance.getJvmArguments());
        }

        cmdLine.addArgument("-cp").addArgument(System.getProperty("java.class.path"))
                .addArgument(NDRunner.class.getName()).addArgument("--slave").addArgument("--executionid")
                .addArgument(instance.getExecutionID().toString());

        System.gc();
        DefaultExecutor executor = new DefaultExecutor();

        startTime = (GregorianCalendar) Calendar.getInstance();
        startTimeMS = startTime.getTimeInMillis();

        try {
            exitCode = executor.execute(cmdLine);
            result.setSuccessfulTermination(true);
            result.setExitCode(exitCode);
        } catch (ExecuteException e) {
            result.setSuccessfulTermination(false);
            result.setExitCode(e.getExitValue());
        } finally {
            endTimeMS = System.currentTimeMillis();
        }

        result.setExecutionTime(endTimeMS - startTimeMS);
        result.setStartTime(startTime);

        System.out.println(result);

        return result;
    }

    protected TestSuite loadTestConfiguration() throws InvalidTestXMLConfigurationException {
        Package pkg = TestSuite.class.getPackage();
        TestSuite suite = null;

        try {
            JAXBContext jaxbContext = JAXBContext.newInstance(pkg.getName());

            Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();

            suite = (TestSuite) unmarshaller.unmarshal(configResource);
        } catch (JAXBException e) {
            throw new InvalidTestXMLConfigurationException(
                    "Error while " + "reading the test configuration file " + configResource.toExternalForm(), e);
        }

        return suite;
    }

    /**
     * Searches the classpath for the standard test configuration
     * file.
     *
     * @see #THOTTI_STANDARD_TEST_CONFIGURATION
     */
    protected URL findTestConfiguration() throws NoTestConfigurationException {
        ClassLoader myLoader = getClass().getClassLoader();

        URL config = myLoader.getResource(THOTTI_STANDARD_TEST_CONFIGURATION);

        if (null == config) {
            throw new NoTestConfigurationException(
                    "Standard configuration file " + THOTTI_STANDARD_TEST_CONFIGURATION + " "
                            + "not found as resource with the current " + "classpath settings.");
        }

        return config;
    }

    void executeSingleTest(NonDistributedTestType test, TestInstanceType instance)
            throws IOException, TestMethodSignatureMismatchException, NoSuchTestClassException,
            AmbiguousTestMethodException, NoSuchTestMethodException {

        SingleTestExecution ste = new SingleTestExecution().withClass(test.getClazz()).withMethod(test.getMethod())
                .withInstance(instance);

        try {
            ste.run();
        } catch (NoSuchTestClassException e) {
            System.err.println(e.getMessage());
            System.exit(TEST_CLASS_NOT_FOUND);
        } catch (NoSuchTestMethodException e) {
            System.err.println(e.getMessage());
            System.exit(TEST_METHOD_NOT_FOUND);
        } catch (AmbiguousTestMethodException re) {
            System.err.println(re.getMessage());
            System.exit(TEST_METHOD_AMBIGOUS);
        } catch (TestMethodSignatureMismatchException e) {
            System.err.println(e.getMessage());
            System.exit(TEST_METHOD_SIGNATURE_MISMATCH);
        }
    }

    public File generateResultFileName() {
        return new File(THOTTI_RESULT_FILE);
    }

    /**
     * Internal factory method to create a fresh instance
     * of {@link SingleTestExecution}.
     * <p/>
     * This method exists only to support mocking in unit tests
     * better.
     *
     * @return new instance of {@link SingleTestExecution}.
     */
    SingleTestExecution createSingleTestExecution() {
        return new SingleTestExecution();
    }

    public String getHostname() {
        try {
            InetAddress localMachine = InetAddress.getLocalHost();

            return localMachine.getHostName();
        } catch (java.net.UnknownHostException uhe) {
            return null;
        }
    }

    public String getGenerationDateAndTime() {
        Date now = new Date();
        SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssz");

        return formatter.format(now);
    }

    private String generateResultUUID() {
        String uuid = UUID.randomUUID().toString();

        return uuid;
    }

    private class FindResult extends TestInstanceType {
        NonDistributedTestType test;
        TestInstanceType instance;

        public FindResult(NonDistributedTestType t, TestInstanceType i) {
            super();
            test = t;
            instance = i;
        }
    }
}