edu.odu.cs.cs350.yellow1.jar.ExecuteJar.java Source code

Java tutorial

Introduction

Here is the source code for edu.odu.cs.cs350.yellow1.jar.ExecuteJar.java

Source

/**
 *   Copyright (C) 2014  John Berlin
 *
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 *   along with this program.  If not, see http://www.gnu.org/licenses/
 */
package edu.odu.cs.cs350.yellow1.jar;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.MessageDigest;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Queue;
import java.util.concurrent.Callable;

import org.apache.commons.exec.CommandLine;
import org.apache.commons.exec.DefaultExecutor;
import org.apache.commons.exec.ExecuteException;
import org.apache.commons.exec.ExecuteWatchdog;
import org.apache.commons.exec.Executor;
import org.apache.commons.exec.PumpStreamHandler;
import org.apache.commons.io.FileUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

/**
 * Fork a jvm process for each jar to be run and kill the process when it exits
 * <br>To allow for the mutation analysis phase to go by quickly run all mutants in parallel(limited to the number of available cpus)
 * <br>Implements the {@link Callable} interface to allow for this concurrency
 * <br>Takes a mutant jar and runs all tests in the suit on it 
 * <br>Captures the output created by executing the mutant on a test
 * <br>Compares the output created by a mutant when a successful jvm exit occurs 
 * Considered a one shot class also how do you do a unit test for Callable
 * 
 * @author jberlin
 *
 */
public class ExecuteJar implements Callable<ExecutionResults> {
    private final boolean isWindows = System.getProperty("os.name").toLowerCase().contains("windows");
    private final String javaExecutable = isWindows ? "java.exe" : "java";
    private final String pathToJVM = System.getProperty("java.home") + File.separator + "bin" + File.separator
            + javaExecutable;
    private static final Logger logger = LogManager.getLogger(ExecuteJar.class);

    private File jar;

    private Executor executor;
    private ExecuteWatchdog wDog;
    private PumpStreamHandler streamHandler;
    private FileOutputStream log;
    private FileOutputStream err;

    private Queue<File> tests;
    private List<File> didNotExecute;
    private List<File> outFiles;
    private List<File> errFiles;

    private File killedMutant = null;
    private String pathToOutputDir;
    private String jarName;
    private String pathToGold;
    private int numOfSucesses = 0;
    private int numOfFailurs = 0;
    private int testNumKilledME = 0;
    private byte[] goldHash;
    private boolean success;
    private boolean testMore = true;
    private boolean killed = false;

    /**
     * Construct a new instance of ExecuteJar
     * @param jar the file object representing the mutant jar to run tests on
     * @param pathToGold the absolutepath to the gold output
     * @param pathToOutputDir the path to output directory
     * @param tests List of file objects reprenting the tests to run on the mutant jar
     */
    public ExecuteJar(File jar, String pathToGold, String pathToOutputDir, List<File> tests) {
        this.jar = jar;
        this.pathToOutputDir = pathToOutputDir;
        this.pathToGold = pathToGold;
        this.tests = new ArrayDeque<>(tests);
        this.jarName = jar.getName().substring(0, jar.getName().indexOf("."));
        this.didNotExecute = new ArrayList<>();
        this.errFiles = new ArrayList<>();
        this.outFiles = new ArrayList<>();
    }

    /**
     * 
     * {@inheritDoc}
     * <br>Run all tests in the test suit on the mutant 
     * capturing the output created and if the execution of the mutant with a test exits successfully compare the standard output generated by<br>
     * the mutant if different stop running tests and return {@link ExecutionResults}
     * <br> Treats exiting the jvm with error as was not killed continue to run more tests 
     * @return {@link ExecutionResults}
     */
    @Override
    public ExecutionResults call() throws Exception {
        //create new Executor for monitoring mutation running
        executor = new DefaultExecutor();
        //get a MessageDigest Instance for use in comparing outputs
        MessageDigest mDigest = MessageDigest.getInstance("MD5");
        //get file object for gold file
        File f = new File(pathToGold);
        //get the hash value for the gold file
        goldHash = mDigest.digest(FileUtils.readFileToByteArray(f));
        //reset the MessageDigest
        mDigest.reset();
        int testCount = 0;
        //Create a new ExecuteWatchdog with timeout at 10 seconds
        wDog = new ExecuteWatchdog(10000);
        executor.setWatchdog(wDog);
        //loop through the tests till empty
        while (!tests.isEmpty()) {
            //get the next test
            File test = tests.poll();//poll removes the test from the queue
            //prepair captured output files
            String testName = test.getName();
            testName = testName.toUpperCase(Locale.getDefault()).substring(0, testName.indexOf("."));
            String outName = jarName + "_" + testName + "_out.txt";
            String errOutName = jarName + "_" + testName + "_err.txt";
            //create file objects to be written to 
            File standardOut = new File(pathToOutputDir + File.separator + outName);
            File standardErr = new File(pathToOutputDir + File.separator + errOutName);
            //file streams create the files for me
            try {
                log = new FileOutputStream(standardOut);

                err = new FileOutputStream(standardErr);
            } catch (FileNotFoundException e1) {
                logger.error("log or err file not found for jar " + jarName, e1.getMessage());
            }
            //create new stream handler for each execution
            streamHandler = new PumpStreamHandler(/* standard out */log, /* error out */err);
            executor.setStreamHandler(streamHandler);
            //construct the executable command
            CommandLine args = new CommandLine(pathToJVM);
            args.addArgument("-jar");
            args.addArgument(jar.getAbsolutePath());
            args.addArgument(test.getAbsolutePath());
            //new process destroyer per execution
            ShutDownSpawnedJVMProcess killJVM = new ShutDownSpawnedJVMProcess("java -jar " + jarName, 10000);
            killJVM.setWaitOnShutdown(true);
            executor.setProcessDestroyer(killJVM);
            success = false;

            try {
                streamHandler.start();
                int result = executor.execute(args);
                logger.info(jarName + " Sucess with val=[" + result + "] for test[" + testName + "]");
                success = true;
            } catch (ExecuteException ee) {
                logger.error(jarName + " Execute exception " + ee.getMessage() + " with val=[" + ee.getExitValue()
                        + "] for test[" + testName + "]");
            } catch (IOException e) {
                logger.error(jarName + " IOExecption " + e.getMessage());

            } finally {
                //PumpStreamHandler does not guarantee the closing of stream 100% so to release the locks held by the filestreams 
                //on the created output files so close manually 
                //if the streamhandler was able to close then this will through exception which we ignore
                try {
                    streamHandler.stop();
                    //log.flush();
                    log.close();
                    //err.flush();
                    err.close();
                } catch (IOException e) {
                    logger.error(e.getMessage());
                    //ignore nothing I can do 
                }

            }

            //if the spawned process exited with success value 
            //check the hash of the output file and delete the empty error file
            //if the hash is different the mutant was killed otherwise test more
            //if the spawned process exited with an error value delete empty standard out file and test more
            if (success) {
                ++numOfSucesses;
                standardErr.delete();
                outFiles.add(standardOut);
                if (!Arrays.equals(goldHash, mDigest.digest(FileUtils.readFileToByteArray(standardOut)))) {
                    testMore = false;
                    logger.debug("Different hashes for jar [" + jarName + "] for test [" + testName + "]");
                } else {
                    logger.debug("Same hashes for jar [" + jarName + "] for test [" + testName + "]");
                }
                mDigest.reset();
            } else {
                ++numOfFailurs;
                standardOut.delete();
                errFiles.add(standardErr);
                this.didNotExecute.add(test);
            }
            ++testCount;
            //the mutant was killed so stop testing 
            if (!testMore) {
                testMore = false;
                killed = true;
                testNumKilledME = testCount;
                break;
            }

        }

        jar.delete();
        return new ExecutionResults(numOfSucesses, numOfFailurs, testNumKilledME, jarName, killed, killedMutant,
                outFiles, errFiles);

    }

}