de.unisb.cs.st.javalanche.mutation.analyze.MutationScoreAnalyzer.java Source code

Java tutorial

Introduction

Here is the source code for de.unisb.cs.st.javalanche.mutation.analyze.MutationScoreAnalyzer.java

Source

/*
 * Copyright (C) 2011 Saarland University
 *
 * This file is part of Javalanche.
 *
 * Javalanche is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Javalanche 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 Public License for more details.
 *
 * You should have received a copy of the GNU Lesser Public License
 * along with Javalanche.  If not, see <http://www.gnu.org/licenses/>.
 */
package de.unisb.cs.st.javalanche.mutation.analyze;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.HashSet;
import java.util.TreeSet;
import java.util.HashMap;
import java.util.Map;

import org.apache.commons.io.FileUtils;
import org.apache.log4j.Logger;

import de.unisb.cs.st.ds.util.io.XmlIo;
import de.unisb.cs.st.javalanche.mutation.analyze.html.HtmlReport;
import de.unisb.cs.st.javalanche.mutation.properties.ConfigurationLocator;
import de.unisb.cs.st.javalanche.mutation.results.Mutation;
import de.unisb.cs.st.javalanche.mutation.results.MutationCoverageFile;
import de.unisb.cs.st.javalanche.mutation.results.MutationTestResult;
import de.unisb.cs.st.javalanche.mutation.results.TestMessage;

/**
 * This analyzer class calculates the mutation score of the class/method units
 * of the system under test. The mutation scores are displayed in the HTML
 * report and the command line interface, though additional information is
 * added to an exported CSV file.
 *
 * The CSV file contains the exact number of mutants generated/covered/killed,
 * the total/killed number of mutation types, the tests that were touched and
 * basic information of the code unit.
 *
 * @author Kevin Jalbert <kevin.j.jalbert@gmail.com>
 */
public class MutationScoreAnalyzer implements MutationAnalyzer {

    private static final Logger logger = Logger.getLogger(MutationResultAnalyzer.class);

    private static final boolean WRITE_FILES = false;

    // Set of maps to hold the data relevant to classes
    private Map<String, Integer> classKilledMutations = new HashMap<String, Integer>();
    private Map<String, Integer> classCoveredMutations = new HashMap<String, Integer>();
    private Map<String, Integer> classTotalMutations = new HashMap<String, Integer>();
    private Map<String, Set<String>> classMutantTests = new HashMap<String, Set<String>>();
    private Map<String, MutationTypeCount> classMutationTypes = new HashMap<String, MutationTypeCount>();

    // Set of maps to hold the data relevant to methods
    private Map<String, Integer> methodKilledMutations = new HashMap<String, Integer>();
    private Map<String, Integer> methodCoveredMutations = new HashMap<String, Integer>();
    private Map<String, Integer> methodTotalMutations = new HashMap<String, Integer>();
    private Map<String, Set<String>> methodMutantTests = new HashMap<String, Set<String>>();
    private Map<String, MutationTypeCount> methodMutationTypes = new HashMap<String, MutationTypeCount>();

    // Set of lists to hold the output of the class/method data
    private List<String> classScores = new ArrayList<String>();
    private List<String> methodScores = new ArrayList<String>();

    /**
     * A data class to aid in the collection of mutation type information. The 
     * total number of occurrences and kills for a mutation type are here.
     *
     * @author Kevin Jalbert <kevin.j.jalbert@gmail.com>
     */
    public class MutationTypeCount {

        public Map<String, Integer> typesTotal = new HashMap<String, Integer>();
        public Map<String, Integer> typesKilled = new HashMap<String, Integer>();

        /**
         * Constructor that creates maps to hold the total/killed mutation types 
         */
        public MutationTypeCount() {
            for (Mutation.MutationType mutationType : Mutation.MutationType.values()) {
                typesTotal.put(mutationType.toString(), 0);
                typesKilled.put(mutationType.toString(), 0);
            }
        }

        /**
         * Increment the passed mutation type by one for the total count.
         *
         * @param mutationType the mutation type to increment
         */
        public void incTypeTotal(String mutationType) {
            Integer count = typesTotal.get(mutationType);
            typesTotal.put(mutationType, count + 1);
        }

        /**
         * Increment the passed mutation type by one for the killed count.
         *
         * @param mutationType the mutation type to increment
         */
        public void incTypeKilled(String mutationType) {
            Integer count = typesKilled.get(mutationType);
            typesKilled.put(mutationType, count + 1);
        }
    }

    /**
     * Iterates through all the mutations and extracts the important data from
     * them. The class and method maps are populated with the extracted data 
     *
     * @param mutations iterable set of mutations from system under test
     */
    private void collectClassMethodData(Iterable<Mutation> mutations) {

        for (Mutation mutation : mutations) {

            if (mutation == null) {
                throw new RuntimeException("Null fetched from db");
            }

            MutationTestResult mutationResult = mutation.getMutationResult();

            Integer count = 0;
            boolean mutationTouched = mutationResult != null && mutationResult.isTouched();
            String mutantClassName = mutation.getClassName();
            String mutantMethodName = mutantClassName + "." + mutation.getMethodName();

            // Handle keeping track of unique tests per method
            Set<String> tests = methodMutantTests.get(mutantMethodName);
            if (tests == null) {
                tests = new HashSet<String>();
            } else {
                if (mutationResult != null) {
                    for (TestMessage testMessage : mutationResult.getAllTestMessages()) {
                        tests.add(testMessage.getTestCaseName());
                    }
                }
            }
            methodMutantTests.put(mutantMethodName, tests);

            // Handle keeping track of unique tests per class
            tests = classMutantTests.get(mutantClassName);
            if (tests == null) {
                tests = new HashSet<String>();
            } else {
                if (mutationResult != null) {
                    for (TestMessage testMessage : mutationResult.getAllTestMessages()) {
                        tests.add(testMessage.getTestCaseName());
                    }
                }
            }
            classMutantTests.put(mutantClassName, tests);

            // Handle counting the total mutants
            count = classTotalMutations.get(mutantClassName);
            if (count == null) {
                classTotalMutations.put(mutantClassName, 1);
                classCoveredMutations.put(mutantClassName, 0);
                classKilledMutations.put(mutantClassName, 0);

                MutationTypeCount mutationTypeCount = new MutationTypeCount();
                mutationTypeCount.incTypeTotal(mutation.getMutationType().toString());
                classMutationTypes.put(mutantClassName, mutationTypeCount);
            } else {
                classTotalMutations.put(mutantClassName, count + 1);
                classMutationTypes.get(mutantClassName).incTypeTotal(mutation.getMutationType().toString());
            }

            // Handle counting the number of mutations per method
            count = methodTotalMutations.get(mutantMethodName);
            if (count == null) {
                methodTotalMutations.put(mutantMethodName, 1);
                methodCoveredMutations.put(mutantMethodName, 0);
                methodKilledMutations.put(mutantMethodName, 0);

                MutationTypeCount mutationTypeCount = new MutationTypeCount();
                mutationTypeCount.incTypeTotal(mutation.getMutationType().toString());
                methodMutationTypes.put(mutantMethodName, mutationTypeCount);
            } else {
                methodTotalMutations.put(mutantMethodName, count + 1);
                methodMutationTypes.get(mutantMethodName).incTypeTotal(mutation.getMutationType().toString());
            }

            // Handle counting covered mutants
            if (mutationTouched) {
                count = classCoveredMutations.get(mutantClassName);
                classCoveredMutations.put(mutantClassName, count + 1);

                count = methodCoveredMutations.get(mutantMethodName);
                methodCoveredMutations.put(mutantMethodName, count + 1);
            }

            // Handle counting the killed mutants
            if (mutation.isDetected()) {

                count = classKilledMutations.get(mutantClassName);
                classKilledMutations.put(mutantClassName, count + 1);

                count = methodKilledMutations.get(mutantMethodName);
                methodKilledMutations.put(mutantMethodName, count + 1);

                classMutationTypes.get(mutantClassName).incTypeKilled(mutation.getMutationType().toString());
                methodMutationTypes.get(mutantMethodName).incTypeKilled(mutation.getMutationType().toString());
            }
        }
    }

    /**
     * Perform mutation score calculations on the collected class data. The
     * collected data is added into the global list to be outputted into a CSV.
     * The data is also appended to the StringBuilder to be outputted to the
     * display.
     *
     * @param sb the StringBuilder to be append all the class data for display
     */
    private void addClassData(StringBuilder sb) {

        for (String className : classTotalMutations.keySet()) {

            int killed = classKilledMutations.get(className);
            int covered = classCoveredMutations.get(className);
            int total = classTotalMutations.get(className);

            double coveredScore = 0;
            if (killed > covered) {
                coveredScore = 1.0; // Limits score when more kills then covered exists (weird case)
            } else if (covered > 0) {
                coveredScore = (double) killed / covered;
            }

            double totalScore = 0;
            if (killed > total) {
                totalScore = 1.0; // Limits score when more kills then total exists (weird case)
            } else if (total > 0) {
                totalScore = (double) killed / total;
            }

            String tests = "";
            for (String test : classMutantTests.get(className)) {
                tests += test + " ";
            }
            tests.trim();

            String typeCounts = "";
            MutationTypeCount unitTypes = classMutationTypes.get(className);
            for (Mutation.MutationType mutationType : Mutation.MutationType.values()) {
                typeCounts += unitTypes.typesKilled.get(mutationType.toString()).toString() + ",";
                typeCounts += unitTypes.typesTotal.get(mutationType.toString()).toString() + ",";
            }

            classScores.add(className + "," + killed + "," + covered + "," + total + "," + coveredScore + ","
                    + totalScore + "," + typeCounts + tests);

            sb.append(formatLine(className + ": ", AnalyzeUtil.formatPercent(killed, covered),
                    AnalyzeUtil.formatPercent(killed, total)));
        }
    }

    /**
     * Perform mutation score calculations on the collected method data. The
     * collected data is added into the global list to be outputted into a CSV.
     * The data is also appended to the StringBuilder to be outputted to the
     * display.
     *
     * @param sb the StringBuilder to be append all the method data for display
     */
    private void addMethodData(StringBuilder sb) {
        for (String methodName : methodTotalMutations.keySet()) {

            int killed = methodKilledMutations.get(methodName);
            int covered = methodCoveredMutations.get(methodName);
            int total = methodTotalMutations.get(methodName);

            double coveredScore = 0;
            if (killed > covered) {
                coveredScore = 1.0; // Limits score when more kills then covered exists (weird case)
            } else if (covered > 0) {
                coveredScore = (double) killed / covered;
            }

            double totalScore = 0;
            if (killed > total) {
                totalScore = 1.0; // Limits score when more kills then total exists (weird case)
            } else if (total > 0) {
                totalScore = (double) killed / total;
            }

            String tests = "";
            for (String test : methodMutantTests.get(methodName)) {
                tests += test + " ";
            }
            tests.trim();

            String typeCounts = "";
            MutationTypeCount unitTypes = methodMutationTypes.get(methodName);
            for (Mutation.MutationType mutationType : Mutation.MutationType.values()) {
                typeCounts += unitTypes.typesKilled.get(mutationType.toString()).toString() + ",";
                typeCounts += unitTypes.typesTotal.get(mutationType.toString()).toString() + ",";
            }

            String className = methodName.substring(0, methodName.lastIndexOf('.'));
            methodScores.add(className + "," + methodName + "," + killed + "," + covered + "," + total + ","
                    + coveredScore + "," + totalScore + "," + typeCounts + tests);

            sb.append(formatLine(methodName.substring(0, methodName.lastIndexOf('(')) + ": ",
                    AnalyzeUtil.formatPercent(killed, covered), AnalyzeUtil.formatPercent(killed, total)));
        }
    }

    /**
     * The analyze method that is called by the analyzeResults task. This method
     * calculates the mutation scores as well as additional test/type information
     * and outputs it to the display and a CSV file.
     *
     * @param mutations iterable set of mutations from system under test
     * @param report the HTML report that is being produced (untouched)
     * @return the class/method mutation score data in a string format
     */
    public String analyze(Iterable<Mutation> mutations, HtmlReport report) {

        // Acquire the CSV list of mutation types (KILLED and TOTAL)
        String mutationTypeString = "";
        for (Mutation.MutationType mutationType : Mutation.MutationType.values()) {
            mutationTypeString += "KILLED_" + mutationType.toString() + ",";
            mutationTypeString += "TOTAL_" + mutationType.toString() + ",";
        }

        collectClassMethodData(mutations);

        // Build up output for class and method data
        StringBuilder sb = new StringBuilder();
        sb.append(formatTitle("----------Class Mutation Score----------"));
        sb.append(formatHeading("Class Name:", "Of Covered", "Of Generated"));
        classScores.add("CLASS_NAME,KILLED_MUTANTS,COVERED_MUTANTS,GENERATED_MUTANTS,"
                + "MUTATION_SCORE_OF_COVERED_MUTANTS,MUTATION_SCORE_OF_GENERATED_MUTANTS," + mutationTypeString
                + "TESTS_TOUCHED");
        addClassData(sb);

        sb.append(formatTitle("----------Method Mutation Score----------"));
        sb.append(formatHeading("Method Name:", "Of Covered", "Of Generated"));
        methodScores.add("CLASS_NAME,METHOD_NAME,KILLED_MUTANTS,COVERED_MUTANTS,GENERATED_MUTANTS,"
                + "MUTATION_SCORE_OF_COVERED_MUTANTS,MUTATION_SCORE_OF_GENERATED_MUTANTS," + mutationTypeString
                + "TESTS_TOUCHED");
        addMethodData(sb);

        // Write collected data to CSV
        try {
            FileUtils.writeLines(
                    new File(
                            ConfigurationLocator.getJavalancheConfiguration().getOutputDir() + "/class-scores.csv"),
                    classScores);
            FileUtils.writeLines(new File(
                    ConfigurationLocator.getJavalancheConfiguration().getOutputDir() + "/method-scores.csv"),
                    methodScores);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

        return sb.toString();
    }

    /**
     * Formats a single line for the class/method mutation score title.
     *
     * @param title the current section title (class/method)
     * @return the formatted string
     */
    private String formatTitle(String title) {
        return String.format("%55s \n", title);
    }

    /**
     * Formats a single line for the class/method mutation score headings.
     *
     * @param type the class/method name
     * @param covered the mutation score of the covered mutants
     * @param generated the mutation score of the generated mutants
     * @return the formatted string
     */
    private String formatHeading(String type, String covered, String generated) {
        return String.format("%-53s %12s, %12s\n", type, covered, generated);
    }

    /**
     * Formats a single line for the class/method mutation score result.
     *
     * @param name the class/method name
     * @param covered the mutation score of the covered mutants
     * @param generated the mutation score of the generated mutants
     * @return the formatted string
     */
    private static String formatLine(String name, String covered, String generated) {
        return String.format("%-53s %12s, %12s\n", name, covered, generated);
    }
}