org.alfresco.bm.test.TestRun.java Source code

Java tutorial

Introduction

Here is the source code for org.alfresco.bm.test.TestRun.java

Source

/*
 * Copyright (C) 2005-2013 Alfresco Software Limited.
 *
 * This file is part of Alfresco
 *
 * Alfresco is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Alfresco is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
 */
package org.alfresco.bm.test;

import java.io.IOException;
import java.util.HashSet;
import java.util.Properties;
import java.util.Set;

import org.alfresco.bm.event.EventProcessor;
import org.alfresco.bm.exception.ObjectNotFoundException;
import org.alfresco.bm.log.LogService;
import org.alfresco.bm.log.LogService.LogLevel;
import org.alfresco.bm.server.EventController;
import org.alfresco.bm.test.mongo.MongoTestDAO;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.bson.types.ObjectId;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.AbstractXmlApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.PropertiesPropertySource;

import com.mongodb.BasicDBList;
import com.mongodb.DBCursor;
import com.mongodb.DBObject;
import com.mongodb.MongoException;

/**
 * Manages the actual execution of a test run.
 * 
 * @author Derek Hulley
 * @since 2.0
 */
public class TestRun implements TestConstants {
    private static Log logger = LogFactory.getLog(TestRun.class);

    private final MongoTestDAO testDAO;
    private final LogService logService;
    private final ObjectId id;
    private final ApplicationContext parentCtx;
    private final String driverId;
    private AbstractXmlApplicationContext testRunCtx; // This will be created when the test run actually starts
    private String test; // Only populated once the test run starts
    private String run; // Only populated once the test run starts
    private String release; // Only populated once the test run starts
    private Integer schema; // Only populated once the test run starts

    /**
     * @param testDAO               data persistence
     * @param logService            logging
     * @param id                    the id of the test that this run controls
     * @param parentCtx             the parent context for all test runs
     * @param driverId              the ID of the driver controlling the test run
     */
    public TestRun(MongoTestDAO testDAO, LogService logService, ObjectId id, ApplicationContext parentCtx,
            String driverId) {
        this.testDAO = testDAO;
        this.logService = logService;
        this.id = id;
        this.parentCtx = parentCtx;
        this.driverId = driverId;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((id == null) ? 0 : id.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        TestRun other = (TestRun) obj;
        if (id == null) {
            if (other.id != null)
                return false;
        } else if (!id.equals(other.id))
            return false;
        return true;
    }

    @Override
    public String toString() {
        StringBuilder builder = new StringBuilder();
        builder.append("TestRun [id=");
        builder.append(id);
        builder.append("]");
        return builder.toString();
    }

    /**
     * @return              the ID of the test run being monitored
     */
    public ObjectId getId() {
        return id;
    }

    /**
     * @return the raw DBObject or <tt>null</tt> if the test run is no longer
     *         valid
     */
    private DBObject getRunObj(boolean includeProperties) {
        try {
            DBObject runObj = testDAO.getTestRun(id, includeProperties);
            return runObj;
        } catch (ObjectNotFoundException e) {
            if (logger.isDebugEnabled()) {
                logger.debug("The test run '" + id + "' no longer exists.");
            }
            stop();
            return null;
        }
    }

    /**
     * Get the application context associated with the test run.
     * 
     * @return              the application context or <tt>null</tt> if the application context
     *                      has not been initialized or has already been shut down.
     */
    public synchronized ApplicationContext getCtx() {
        return testRunCtx;
    }

    /**
     * Called to do a check of the test run state and make any adjustments as necessary.
     * <p/>
     * The initial state is checked and we only accept states that have any chance of
     * progression or execution.
     */
    public synchronized void checkState() {
        if (logger.isDebugEnabled()) {
            logger.debug("Checking test run state: " + id);
        }
        DBObject runObj = getRunObj(false);
        if (runObj == null) {
            return;
        }
        Integer version = (Integer) runObj.get(FIELD_VERSION);
        // Current state
        String stateStr = (String) runObj.get(FIELD_STATE);
        TestRunState state = TestRunState.valueOf(stateStr);

        long now = System.currentTimeMillis();
        // Calculate duration
        Long started = (Long) runObj.get(FIELD_STARTED);
        Long duration = (started == null || started < 0L) ? null : (now - started);

        // We update the test run according to its current state and possibly progress it to a new state
        switch (state) {
        case NOT_SCHEDULED:
            // Nothing to do
            return;
        case SCHEDULED:
            // Check if the time execution time has been reached
            Long scheduled = (Long) runObj.get(FIELD_SCHEDULED);
            if (scheduled == null) {
                logger.error("Scheduled state has been reached without a scheduled time: " + runObj);
                testDAO.updateTestRunState(id, version, TestRunState.STOPPED, -1L, -1L, now, -1L, null, null, null,
                        null);
            } else if (scheduled < now) {
                // Start the context
                start();
                if (testRunCtx == null) {
                    // It failed to start.  Errors will have been reported.
                    // now stop the test (otherwise it will restart again and again ...) - fkb 2015-11-10 
                    now = System.currentTimeMillis();
                    boolean stopped = testDAO.updateTestRunState(id, version, TestRunState.STOPPED, now, now, now,
                            null, 1L, 0.0D, 0L, 0L);
                    if (logger.isDebugEnabled()) {
                        if (stopped) {
                            logger.debug("Successfully switched test run to " + TestRunState.STOPPED + ": " + id);
                        } else {
                            logger.debug("Failed to transition test run to " + TestRunState.STOPPED + ": " + id);
                        }
                    }
                    return;
                }

                // It has been scheduled and the time has passed
                boolean changed = testDAO.updateTestRunState(id, version, TestRunState.STARTED, null, now, null,
                        null, null, 0.0D, 0L, 0L);
                if (!changed) {
                    if (logger.isDebugEnabled()) {
                        logger.debug("Failed to transition test run to " + TestRunState.STARTED + ": " + id);
                    }
                    // We bug out until the next check cycle
                    return;
                }

                // Lock in all properties
                try {
                    runObj = getRunObj(false);
                    if (runObj == null) {
                        return;
                    }
                    ObjectId testObjId = (ObjectId) runObj.get(FIELD_TEST);

                    testDAO.lockProperties(testObjId, id);
                } catch (ObjectNotFoundException onfe) {
                    logger.error("Unable to find test or lock properties.", onfe);
                }

                // Give the EventController the updated list of drivers
                updateDriverIds();

                if (logger.isInfoEnabled()) {
                    logger.info("Transitioned test run to " + TestRunState.STARTED + ": " + id);
                }
            }
            return;
        case STARTED:
            start();
            // Monitor progress
            if (testRunCtx != null) {
                // Update the progress
                CompletionEstimator ce = (CompletionEstimator) testRunCtx.getBean("completionEstimator");
                double progress = ce.getCompletion();
                boolean completed = ce.isCompleted();
                long resultsSuccess = ce.getResultsSuccess();
                long resultsFail = ce.getResultsFail();
                if (completed) {
                    testDAO.updateTestRunState(id, version, TestRunState.COMPLETED, null, null, null, now, duration,
                            progress, resultsSuccess, resultsFail);
                    // We do not need another ping.  We can just shut down the context now.
                    stop();
                } else {
                    testDAO.updateTestRunState(id, version, null, null, null, null, null, duration, progress,
                            resultsSuccess, resultsFail);
                    // Give the EventController the updated list of drivers
                    updateDriverIds();
                }
                // There is nothing to do, the context is already available
                return;
            }
            return;
        case STOPPED:
            stop();
            return;
        case COMPLETED:
            stop();
            return;
        }
    }

    /**
     * Helper method to inject the current list of driver IDs into the EventController.
     * The test run must already have started for this to work.
     */
    private synchronized void updateDriverIds() {
        // Give the EventController the updated list of drivers
        DBCursor cursor = testDAO.getDrivers(release, schema, true);
        String[] driverIds = new String[cursor.size()];
        int index = 0;
        while (cursor.hasNext()) {
            ObjectId driverIdObj = (ObjectId) cursor.next().get(FIELD_ID);
            driverIds[index++] = driverIdObj.toString();
        }
        cursor.close();
        // Pass the driver IDs to the event controller
        EventController eventController = testRunCtx.getBean(EventController.class);
        eventController.setDriverIds(driverIds);
    }

    /**
     * Called to ensure that the application context is started.
     * <p/>
     * Note that we only pull out the test and test run names at this point so that we don't end up
     * using stale data.
     */
    private synchronized void start() {
        DBObject runObj = getRunObj(true);
        if (runObj == null) {
            // Nothing much we can do here
            return;
        }

        // Check the application context
        if (testRunCtx != null) {
            // There is nothing to do, the context is already available
            return;
        }

        // INFO logging as this is a critical part of the whole application
        if (logger.isInfoEnabled()) {
            logger.info("Starting test run application context: " + runObj);
        }

        ObjectId testObjId = (ObjectId) runObj.get(FIELD_TEST);
        ObjectId runObjId = (ObjectId) runObj.get(FIELD_ID);
        String run = (String) runObj.get(FIELD_NAME);
        // We need to build the test run FQN out of the test run details
        DBObject testObj = testDAO.getTest(testObjId, false);
        if (testObj == null) {
            logger.warn("The test associated with the test run has been removed: " + runObj);
            logger.warn("The test run will be stopped and deleted: " + id);
            stop();
            testDAO.deleteTestRun(id);
            return;
        }
        String test = (String) testObj.get(FIELD_NAME);
        String release = (String) testObj.get(FIELD_RELEASE);
        Integer schema = (Integer) testObj.get(FIELD_SCHEMA);
        String testRunFqn = test + "." + run;

        // Extract the current properties for the run
        Set<String> propsToMask = new HashSet<String>(7);
        Properties testRunProps = new Properties();
        {
            testRunProps.put(PROP_DRIVER_ID, driverId);
            testRunProps.put(PROP_TEST, test);
            testRunProps.put(PROP_TEST_RUN, run);
            testRunProps.put(PROP_TEST_RUN_ID, id.toString());
            testRunProps.put(PROP_TEST_RUN_FQN, testRunFqn);

            BasicDBList propObjs = (BasicDBList) runObj.get(FIELD_PROPERTIES);
            for (Object obj : propObjs) {
                DBObject propObj = (DBObject) obj;
                String propName = (String) propObj.get(FIELD_NAME);
                String propDef = (String) propObj.get(FIELD_DEFAULT);
                String propValue = (String) propObj.get(FIELD_VALUE);
                if (propValue == null) {
                    propValue = propDef;
                }
                testRunProps.put(propName, propValue);
                // Check on the masking for later reporting
                boolean mask = Boolean.parseBoolean((String) propObj.get(FIELD_MASK));
                if (mask) {
                    propsToMask.add(propName);
                }
            }
        }

        // Create the child application context WITHOUT AUTOSTART
        // TODO: This is hard coded to "config/spring/test-context.xml".  It should be one of the
        //       test definition properties and have the same as default.
        ClassPathXmlApplicationContext testRunCtx = new ClassPathXmlApplicationContext(
                new String[] { PATH_TEST_CONTEXT }, false);
        // When running stand-alone, there might not be a parent context
        if (parentCtx != null) {
            testRunCtx.setParent(parentCtx);
        }
        // Push cluster properties into the context (must be done AFTER setting parent context)
        ConfigurableEnvironment ctxEnv = testRunCtx.getEnvironment();
        ctxEnv.getPropertySources().addFirst(new PropertiesPropertySource("run-props", testRunProps));
        ctxEnv.getPropertySources().addFirst(new PropertiesPropertySource("system-props", System.getProperties()));

        // Complete logging of what is going to be used for the test
        if (logger.isInfoEnabled()) {
            String nl = "\n";
            StringBuilder sb = new StringBuilder(1024);
            sb.append("Test run application context starting: ").append(nl).append("   Run ID:       ").append(id)
                    .append(nl).append("   Test Name:    ").append(test).append(nl).append("   Run Name:     ")
                    .append(run).append(nl).append("   Driver ID:    ").append(driverId).append(nl)
                    .append("   Release:      ").append(release).append(nl).append("   Schema:       ")
                    .append(schema).append(nl).append("   Test Run Properties:   ").append(nl);
            for (Object propNameObj : testRunProps.keySet()) {
                String propName = (String) propNameObj;
                String propValue = testRunProps.getProperty(propName);
                if (propsToMask.contains(propName) || propName.toLowerCase().contains("username")
                        || propName.toLowerCase().contains("password")) {
                    propValue = MASK;
                }
                sb.append("      ").append(propName).append("=").append(propValue).append(nl);
            }
            sb.append("   System Properties:   ").append(nl);
            for (Object propNameObj : System.getProperties().keySet()) {
                String propName = (String) propNameObj;
                String propValue = System.getProperty(propName);
                if (propsToMask.contains(propName) || propName.toLowerCase().contains("username")
                        || propName.toLowerCase().contains("password")) {
                    propValue = MASK;
                }
                sb.append("      ").append(propName).append("=").append(propValue).append(nl);
            }
            logger.info(sb);
        }

        // Now refresh (to load beans) and start
        try {
            this.testRunCtx = testRunCtx;
            // 2015-08-04 fkbecker: store definitions first - for refresh() or start() may fail, too. 
            this.test = test;
            this.run = run;
            this.release = release;
            this.schema = schema;

            testRunCtx.refresh();
            testRunCtx.start();

            // Make sure that the required components are present and of the correct type
            // There may be multiple beans of the type, so we have to use the specific bean name.
            @SuppressWarnings("unused")
            CompletionEstimator estimator = (CompletionEstimator) testRunCtx.getBean("completionEstimator");
            @SuppressWarnings("unused")
            EventProcessor startEventProcessor = (EventProcessor) testRunCtx.getBean("event.start");

            // Register the driver with the test run
            testDAO.addTestRunDriver(runObjId, driverId);

            // Log the successful startup
            logService.log(driverId, test, run, LogLevel.INFO,
                    "Successful startup of test run '" + testRunFqn + "'.");
        } catch (Exception e) {
            /*
            Throwable root = ExceptionUtils.getRootCause(e);
            if (root != null && (root instanceof MongoException || root instanceof IOException))
            {
            // 2015-08-04 fkbecker IOException also thrown by FTP file service if host not reachable ...
            // FIXME 
                
            String msg1 = "Failed to start test run application '" + testRunFqn + "': " + e.getCause().getMessage();
            //String msg2 = "Set the test run property '" + PROP_MONGO_TEST_HOST + "' (<server>:<port>) as required.";
            // We deal with this specifically as it's a simple case of not finding the MongoDB
            logger.error(msg1);
            //logger.error(msg2);
            logService.log(driverId, test, run, LogLevel.ERROR, msg1);
            //logService.log(driverId, test, run, LogLevel.ERROR, msg2);
            }
            else
            {*/
            String stack = ExceptionUtils.getStackTrace(e);
            logger.error("Failed to start test run application '" + testRunFqn + "': ", e);
            String error = "Failed to start test run application '" + testRunFqn + ". \r\n" + stack;
            logService.log(driverId, test, run, LogLevel.ERROR, error);
            //}
            stop();
        }
    }

    /**
     * Called to forcibly stop all executing test runs
     */
    public synchronized void stop() {
        // Check the application context
        if (testRunCtx == null) {
            // There is nothing to do, the context is not available
            return;
        }

        // INFO logging as this is a critical part of the whole application
        if (logger.isInfoEnabled()) {
            logger.info("Stopping test run application context: " + id);
        }

        try {
            DBObject runObj = getRunObj(false);
            if (runObj != null) {
                ObjectId runObjId = (ObjectId) runObj.get(FIELD_ID);
                // Unregister the driver from the test run
                testDAO.removeTestRunDriver(runObjId, driverId);
            }

            boolean active = testRunCtx.isActive();
            // Stop the context
            try {
                testRunCtx.stop();
            } catch (IllegalStateException e) {
                // Ignore if we didn't have a functioning context to start with
                if (active) {
                    logger.error("Unable to stop test run context: " + e.getMessage());
                }
            }
            // Close down completely
            try {
                testRunCtx.close();
            } catch (IllegalStateException e) {
                // Ignore if we didn't have a functioning context to start with
                if (active) {
                    logger.error("Unable to close test run context: " + e.getMessage());
                }
            }
        } catch (Exception e) {
            // There is little more that we can except report that the child ctx did not shut down
            logger.error("Test run application context did not shut down cleanly: \n" + id, e);
        } finally {
            testRunCtx = null;
        }

        // Log the successful shutdown
        logService.log(driverId, test, run, LogLevel.INFO,
                "Successful shutdown of test run '" + test + "." + run + "'.");
    }
}