Major is an efficient and flexible mutation analysis framework that supports:
generating and embedding mutants during compilation;
exporting source-code mutants;
suppressing equivalent mutants;
computing the mutant-detection rate and the set of live mutants for a given a set of tests;
computing a complete mutant-test detection matrix for a given set of tests.
The Major mutation framework provides three main components:
Mutator: a mutator plugin for the javac
Java compiler.
Mml: a DSL for customizing mutant generation.
Analyzer: a test runner that efficiently determines live mutants, the mutant-detection ratio, or a full mutant-test matrix.
The installation is simple — all you need is included in the Major release package!
Download major.zip (the most recent release).
Unzip major.zip, which creates the major directory:
Verify that you are using a Java-8 compiler by running javac -version
.
The example directory provides two examples for how to use Major to perform mutation analysis:
ant: mutation analysis using Apache Ant.
standalone: mutation analysis using Major standalone.
Execute runAll.sh
within the example directory to run all examples or run.sh
in a subdirectory for a particular example.
This step by step tutorial demonstrates how to use Major for:
All examples in this tutorial use the triangle program (example directory) and the provided mml files (mml directory).
Major’s domain specific language (mml) supports customizing mutant generation. Suppose only return statements, relational operators, and conditional operators should be mutated within the method classify
of the class triangle.Triangle
. The following mml file expresses these requirements:
targetOp{
// Define the replacements for ROR
BIN(>)->{>=,!=,FALSE};
BIN(<)->{<=,!=,FALSE};
BIN(>=)->{>,==,TRUE};
BIN(<=)->{<,==,TRUE};
BIN(==)->{<=,>=,FALSE,LHS,RHS};
BIN(!=)->{<,>,TRUE,LHS,RHS};
// Define the replacements for COR
BIN(&&)->{==,LHS,RHS,FALSE};
BIN(||)->{!=,LHS,RHS,TRUE};
// Define the type of statement that STD should delete
DEL(RETURN);
// Enable the STD, COR, and ROR mutation operators
STD;
COR;
ROR;
}
// Call the defined operator group for the target method
targetOp<"triangle.Triangle::classify(int,int,int)">;
Customizing mml files is optional. Major is released with useful default mml files.
Major’s mml compiler (mmlc) validates an mml file and compiles it into a binary representation:
major$ mmlc tutorial.mml tutorial.mml.bin
Note that the second argument is optional – if omitted, the compiler will add .bin to the input file name.
Major’s mutator uses the compiled mml file (tutorial.mml.bin) to configure the mutant-generation process.
The following command generates and compiles all mutants, using the tutorial.mml.bin file:
major$ major --mml tutorial.mml.bin -d bin src/triangle/Triangle.java
Generated 86 mutants (128 ms)
To invoke Major’s mutator from an Apache Ant compile target, adapt the build.xml build file as follows:
<property name="major.jar" value="<path to major.jar>"/>
<property name="mml" value="<path to compiled mml file>"/>
<target name="compile" depends="init" description="Compile">
<javac srcdir="src" destdir="bin" debug="yes">
<classpath location="${major.jar}"/>
<compilerarg value="-Xplugin:MajorPlugin mml:${mml}"/>
</javac>
</target>
The following command generates and compiles all mutants, using the tutorial.mml.bin file:
major$ ant -Dmml=tutorial.mml.bin compile
compile:
[javac] Compiling 1 source file to bin
[javac] Generated 86 mutants (149 ms)
BUILD SUCCESSFUL
Total time: 0 seconds
Major’s mutator plugin reports the number of generated mutants and the total time (mutant generation and compilation). Additionally, it produces a log file mutants.log, which contains detailed information about the generated mutants. Here is an example for the mutants generated for the tutorial program:
major$ head -3 mutants.log
1:ROR:<=(int,int):<(int,int):triangle.Triangle@classify(int,int,int):11:a <= 0 |==> a < 0
2:ROR:<=(int,int):==(int,int):triangle.Triangle@classify(int,int,int):11:a <= 0 |==> a == 0
3:ROR:<=(int,int):TRUE(int,int):triangle.Triangle@classify(int,int,int):11:a <= 0 |==> true
Major supports exporting generated mutants to individual source-code files. This feature is disabled by default. See the documentation for more details.
The build.xml file has to provide a suitable test target that enables Major’s analyzer. The analyzer takes the set of generated mutants and executes a given test suite against them. The following mutation.test
target enables the mutation analysis and exports the results to summary.csv, and details.csv (see the documentation for configuration details):
<target name="mutation.test" description="Run mutation analysis">
<echo message="Running mutation analysis ..."/>
<junit printsummary="false"
showoutput="false"
mutationAnalysis="true"
summaryFile="summary.csv"
mutantDetailsFile="details.csv">
<classpath path="bin"/>
<batchtest fork="false">
<fileset dir="test">
<include name="**/*Test*.java"/>
</fileset>
</batchtest>
</junit>
</target>
Using Major’s default analyzer, the following command invokes the mutation.test
target:
major$ ant mutation.test
mutation.test:
[echo] Running mutation analysis ...
[junit] MAJOR: Mutation analysis enabled
[junit] MAJOR: ------------------------------------------------------------
[junit] MAJOR: Run 1 ordered test(s)to verify independence
[junit] MAJOR: ------------------------------------------------------------
[junit] MAJOR: Preprocessing time: 0.02 seconds
[junit] MAJOR: ------------------------------------------------------------
[junit] MAJOR: Serialize preprocessing results to preprocessing.ser
[junit] MAJOR: ------------------------------------------------------------
[junit] MAJOR: Mutants generated: 86
[junit] MAJOR: Mutants covered: 86 (100.00%)
[junit] MAJOR: ------------------------------------------------------------
[junit] MAJOR: Export coverage map to covMap.csv
[junit] MAJOR: Export test map to testMap.csv
[junit] MAJOR: ------------------------------------------------------------
[junit] MAJOR: Run mutation analysis with 86 mutant(s) and 1 test(s).
[junit] MAJOR: ------------------------------------------------------------
[junit] MAJOR: 1/1 - triangle.test.TestSuite (2ms / 86):
[junit] MAJOR: 189 (76 / 86 / 86) -> AVG-RTPM: 2ms
[junit] MAJOR: Mutants killed (FAIL-EXC-TIME) / live: 76 (76-0-0) / 10
[junit] MAJOR: ------------------------------------------------------------
[junit] MAJOR: Summary:
[junit] MAJOR:
[junit] MAJOR: Analysis time : 0.19 seconds
[junit] MAJOR: Mutation score : 88.37% (88.37%)
[junit] MAJOR: Mutants killed (FAIL-EXC-TIME): 76 (76-0-0)
[junit] MAJOR: Mutants live : 10
[junit] MAJOR: Mutant executions : 86
[junit] MAJOR: ------------------------------------------------------------
[junit] MAJOR: Export summary of results to summary.csv
[junit] MAJOR: Export mutant details to details.csv
[junit] MAJOR: ------------------------------------------------------------
[junit] MAJOR: Done!
[junit] MAJOR: ------------------------------------------------------------
BUILD SUCCESSFUL
Total time: 0 seconds
As configured in the build.xml file, the results of the mutation analysis are exported to the files summary.csv and details.csv, which provide the following information:
summary.csv: Summary of mutation analysis results.
killed.csv: The reason why a mutant is detected (assertion failure, exception, or timeout) or live.
Major’s mutator is a javac compiler plugin that traverses and translates the abstract syntax tree (AST). All mutants are embedded into the AST and compiled to byte-code. Individual mutants can be enabled at runtime without recompilation.
The mutator plugin supports the following configuration options:
mml:<FILE>
: run mutators specified in the compiled mml file
mutants.log:<FILE>
: The location of the mutants.log file (default: ./mutants.log)
export.mutants
: If set, Major exports each generated mutant as a source code file
mutants.directory:<DIR>
: The export directory for the source-code mutants (default: ./mutants)
export.context
: If set, Major exports context information for each generated mutant
context.file:<FILE>
: The location of the mutant-context file (default: ./mutants.context)
strict.checks
: If set, Major discards mutants that would not be compilable if created at the source-code level.
enable.decl.refactor
: If set, Major attempts to refactor large static declarations that it thinks will trigger a “code too large” error from the inclusion of mutations
decl.refactor.params:MAX_TOTAL_ELEMS,MAX_INDIVIDUAL_ELEMS
: a comma seperated list of refactor parameters to override defaults
MAX_TOTAL_ELEMS
: the threshold total number of mutated constants across all declarations in any given method (default 1000)MAX_INDIVIDUAL_ELEMS
: the threshold number of mutated constants for any given declaration (default 50)enable.method.refactor
: If set, Major attempts to refactor large methods that it thinks will trigger a “code too large” error from the inclusion of mutations
method.refactor.params:MUTANT_THRESHOLD,MUTANTS_PER_METHOD
: a comma seperated list of refactor parameters to override defaults
MUTANT_THRESHOLD
: the threshold total number of mutants in a method to trigger a refactor (default 1000)MUTANTS_PER_METHOD
: the number of mutants to allow in the refactored versions of large methods (default 250)logging:ARG1,ARG2,ARG3...
: a semicolon-separated argument list
file:FILE
specify the file name to save the log tofile-level:LEVEL
specify the minimal level to write to fileconsole-level:LEVEL
specify the minimal level to log to consoleconsole-format:LEVEL
specify the formatter for console loggingfile-format:LEVEL
specify the formatter for file loggingLogging levels: all
, finest
, finer
, fine
, warning
, severe
, and none
Logging formatters: xml
, color
, verbose-color
, oneline
, verbose
, simple
Mml files define which mutation operators to enabled and what program elements to mutate. This allows for a fine-grained definition and a flexible application of mutation operators. Example mml files are provided in the release. Note that Major’s mutator interprets pre-compiled mml files. Use the mml compiler mmlc
to validate and compile a mml file.
Major supports the following mutation operator groups, each of which groups multiple related mutation operators:
Replaces a binary arithmetic operator with compatible alternatives.
Examples:
a + b
==> a - b
a % b
==> a * b
Replaces a conditional operator with compatible alternatives. Also replaces atomic boolean conditions with true
and false
(e.g., if(flag)
or if(isSet())
.
Examples:
a || b
==> a && b
if(flag)
==> if(true)
Replaces a binary logical operator with compatible alternatives.
Examples:
a ^ b
==> a | b
Replaces a relational operator with compatible alternatives.
Examples:
a == b
==> a >= b
Replaces a bit-shift operator with compatible alternatives.
Examples:
a >> b
==> a << b
Replaces a unary operator with compatible alternatives.
Examples:
-a
==> ~a
Replaces an expression (in an otherwise unmutated statement) with a default value.
Examples:
return a
==> return 0
int a = b
==> int a = 0
return
break
continue
return a
break
continue
foo(a,b)
a = b
++a
a--
Major’s mutator generates a log file mutants.log, which provides detailed information about the generated mutants and uses a colon (‘:’) as separator. The log file contains one row per generated mutant; each row contains 7 columns with the following information:
Mutants’ unique number (id)
Name of the applied mutation operator
Original operator symbol
Replacement operator symbol
Fully qualified name of the mutated method
Line number in original source file
Summary of the applied transformation (<from> |==> <to>
)
Here is a log-entry example for an ROR mutation that has the mutant id 11 and was generated on line 18 in a method named classify
of the class Triangle
:
11:ROR:<=(int,int):<(int,int):Triangle@classify:18:a <= 0 |==> a < 0
Mutants can be exported as individual source-code files. This feature is disabled by default; to enable it, set the export.mutants option. If enabled, Major duplicates the original source file for each mutant, injects the mutant into the copy, and exports the resulting faulty copy (to ./mutants
by default). Use the mutants.directory:<DIR>
option to control the output directory. Major automatically creates the export directory and parent directories if necessary.
Note that mutating a large code base and exporting all mutants to individual source files increases the mutation/compilation time and requires significantly more disk space than the log file.
Major’s compiler plugin can be used standalone or in a build system.
TODO
TODO
Consider the following compile
target in an Apache Ant build.xml file:
<target name="compile" depends="init" description="Compile">
<javac srcdir="src" destdir="bin" debug="yes">
</javac>
</target>
Add the following options to enable Major’s mutator plugin:
<property name="major.jar" value="<path to major.jar>"/>
<property name="mml" value="<path to compiled mml file>"/>
<target name="compile" depends="init" description="Compile">
<javac srcdir="src" destdir="bin" debug="yes">
<classpath location="${major.jar}"/>
<compilerarg value="-Xplugin:MajorPlugin mml:${mml}"/>
</javac>
</target>
Mutated class files reference Major’s run-time configuration class to access mutant identifiers (_M_NO
) and/or monitor mutation coverage (COVERED
). The archive and source files of the default implementation are provided in the config directory. It can be extended to perform additional analyses. Note that the configuration class does not need to be available on the classpath during mutant generation (the mutator does not try to resolve this class at compile-time, but rather creates AST nodes for mutant identifiers and coverage invocations based on the interface defined by this class.
Major’s domain-specific language mml supports fine-grained configuration of the mutant generation process. A mml file contains a sequence of statements, where each statement represents one of:
Variable definition, e.g., for scopes or replacements.
Replacement definition for mutation operators.
Definition of statement types for the STD mutation operator.
Definition of literal types for the LVR mutation operator.
Invocation of a pre-defined or custom mutation operator (group).
Definition of a custom mutation operator.
Line comment.
The first five statement types are terminated by a semicolon, an operator group definition is encapsulated by curly braces, and a line comment is terminated by the end-of-line.
Mml provides statement scopes for replacement definitions and operator invocations to support the mutation of a certain package, class, or method within a program. The following diagram shows the definition of a statement scope:
Scope corresponds to a package, class, or method. A scope is defined as a fully qualified name – referred to as flatname. A flatname can be either provided as a quoted String or as a variable. Note that a statement scope is optional. If no statement scope is provided, the corresponding definition or invocation is applied to the root package.
The following diagram shows the syntax rules for a flatname:
The naming conventions for valid identifiers (IDENT
) follow those of the Java programming language, given that a flatname identifies a program element. The following four examples show valid flatnames for a package, a class, a set of overloaded methods, and a particular method:
"java.lang"
"java.lang.String"
"java.lang.String@substring"
"java.lang.String::substring(int,int)"
The flatname syntax also supports the identification of innerclasses and constructors, consistent with the naming conventions of Java. For Example, the subsequent definitions address an inner class, a constructor, and a static class initializer:
"foo.Bar$InnerClass"
"foo.Bar@<init>"
"foo.Bar@<clinit>"
For a given scope, mutation operators can be enabled (+
, which is the default if the flag is omitted, or disabled (-
). In the following example, the AOR mutation operator is generally enabled for the package org
but specifically disabled for the class Foo
within that package:
+AOR<"org">;
-AOR<"org.Foo">;
Note that the flag (prefix) for enabling/disabling operators is optional. The default flag (+
) for enabling operators improves readability but can be omitted. That is, +AOR<"org">;
and AOR<"org">;
are equivalent statements.
For replacement definitions, there are two possibilities: Individual replacements can be added (+
) to an existing list or the entire replacement list can be overridden (!
) – the latter is the default if this optional flag is omitted. In the following example, the general definition of replacements for the package org
is extended for the class Foo
but overriden for the class Bar
. (The replacement lists that are effectively applied to the package and classes are given in comments.)
BIN(*)<"org"> -> {+,/}; // * -> {+,/}
+BIN(*)<"org.Foo"> -> {%}; // * -> {+,/,%}
!BIN(*)<"org.Bar"> -> {-}; // * -> {-}
Mml allows the definition of custom operator groups to minimize code duplication (e.g., identical definitions for multiple scopes). A custom operator group may contain any statement that is valid in an mml file, except for a call of another custom operator group. A custom operator group has a unique identifier and its statements are enclosed by curly braces:
myOp {
BIN(*) -> {+,/};
AOR;
}
The following example mml file performs three tasks:
Define specific replacements for AOR and ROR.
Invoke AOR and ROR, using the defined replacements.
Invoke the LVR operator without restrictions.
// Define own replacement list for AOR
BIN(*) -> {/,%};
BIN(/) -> {*,%};
BIN(%) -> {*,/};
// Define own replacement list for ROR
BIN(>) -> {<=,!=,==};
BIN(==) -> {<,!=,>};
// Define types of literals that should be mutated by the LVR operator.
// Literal type is one of {BOOLEAN, NUMBER, STRING}.
LIT(NUMBER);
LIT(BOOLEAN);
// Enable and invoke mutation operators
AOR;
ROR;
LVR;
The next example uses the scoping feature in line 8 and 13-20, and defines a variable in line 11 to avoid code duplication in the subsequent scope declarations. Both features are useful if only a certain package, class, or method should be mutated.
// Definitions for the root node
BIN(>=)->{TRUE,>,==};
BIN(<=)->{TRUE,<,==};
BIN(!=)->{TRUE,<,> };
LIT(NUMBER);
LVR;
// Definition for the package org
ROR<"org">;
// Variable definition for the class Foo
foo="org.x.y.z.Foo";
// Scoping for replacement lists
BIN(&&)<foo>->{LHS,RHS,==,FALSE};
BIN(||)<foo>->{LHS,RHS,!=,TRUE };
// Scoping for mutation operators
-LVR<foo>;
ROR<foo>;
COR<foo>;
The last example demonstrates the custom operator feature, which is useful if the same group of operations (definitions or invocations) should be applied to multiple scopes.
myOp{
// Definitions for the operator group
BIN(>=)->{TRUE,>,==};
BIN(<=)->{TRUE,<,==};
BIN(!=)->{TRUE,<,> };
BIN(&&)->{LHS,RHS,==,FALSE};
BIN(||)->{LHS,RHS,!=,TRUE };
// Mutation operators enabled in this group
ROR;
COR;
}
// Calls of the defined operator group
myOp<"org">;
myOp<"de">;
myOp<"com">;
Major provides a default analyzer, which extends the Apache Ant junit task. This analyzer supports JUnit 3 and 4 tests. % Note that this analyzer does currently not support forking a JVM when executing JUnit tests, meaning that the fork
option must be set to false
.
This table summarizes the configuration options for Major’s default analyzer. (Please refer to the official documentation of the junit task for all other options.)
Option | Description | Values | Default |
---|---|---|---|
mutationAnalysis | Enable mutation analysis. If set to false, all options are ignored. | [true|false] |
false |
analysisType | Run preprocessing and mutation analysis or only one of the two. | [preproc_mutation|preproc|mutation] |
preproc_mutation |
serializedMapsFile | File name for serialized preprocessing results. | <String> |
preprocessing.ser |
mutantsLogFile | The path for the mutants.log file produced my Major’s mutator. | <String> |
mutants.log |
testOrder | Order and granularity of tests. | [original|random|sort_classes|sort_methods] |
original |
debug | Enable debugging output. | [true|false] |
false |
timeoutFactor | Multiplier for the (original) test runtime to compute the test timeout. | <int> |
8 |
timeoutOffset | Offset in ms added to the computed test timeout. | <int> |
0 |
excludeFailingTests | Exclude all failing tests (haltonfailure must be false). | [true|false] |
true |
excludeMutantsFile | Exclude all mutants listed in this file (1 mutant id per row). | <String> |
null |
includeMutantsFile | Include only mutants listed in this file (1 mutant id per row). | <String> |
null |
excludeTestsFile | Exclude all tests listed in this file (1 test id per row). | <String> |
null |
includeTestsFile | Include only tests listed in this file (1 test id per row). | <String> |
null |
summaryFile | Export summary of results to this file (csv). | <String> |
summary.csv |
executionDetailsFile | Export detailed execution information to this file (csv). | <String> |
null |
mutantDetailsFile | Export classification details for each mutant to this file (csv). | <String> |
null |
covMapFile | File name for mutant-coverage matrix (csv). | <String> |
covMap.csv |
testMapFile | File name for mapping of test id to test name (csv). | <String> |
testMap.csv |
exportKillMap | Export mutant-kill matrix (executes every test on every covered mutant!). | [true|false] |
false |
killMapFile | File name for mutant-kill matrix (csv). | <String> |
killMap.csv |
Allows running both steps of the analysis (preprocessing and mutation), which is the default, or only one of the two. For large code bases for which the mutation analysis itself should be parallelized, running preprocessing just once and caching the result (serializedMapsFile
) will greatly speed up analysis time. If set to mutation
, the serialized maps must be provided.
sort_methods
reports results at test-method granularity.sort_classes
reports results at test-class granularity.original
and random
report results at the granularity as defined by the build.xml file.Most Apache Ant build files provide a test
target, which executes the corresponding unit tests. Even if no such target exists, it can be easily set up to execute a set of given unit tests. The following code snippet shows an example test
target (See junit.html for a detailed description of this task):
<target name="test" description="Run all unit test cases">
<junit printsummary="true"
showoutput="true"
haltonfailure="true">
<formatter type="plain" usefile="true"/>
<classpath path="bin"/>
<batchtest fork="no">
<fileset dir="test">
<include name="**/*Test*.java"/>
</fileset>
</batchtest>
</junit>
</target>
To enable mutation analysis, set the option mutationAnalysis
to true
. For clarity, it is best to duplicate and then adapt an existing test
target.
<target name="mutation.test" description="Run mutation analysis">
<junit printsummary="false"
showoutput="false"
haltonfailure="true"
mutationAnalysis="true"
summaryFile="summary.csv"
mutantDetailsFile="details.csv">
<classpath path="bin"/>
<batchtest fork="no">
<fileset dir="test">
<include name="**/*Test*.java"/>
</fileset>
</batchtest>
</junit>
</target>
Mutation analysis repeatedly executes the tests on mutated code. For performance reasons, consider the following when setting up the mutation analysis target:
Turn off logging output (options showsummary
, showoutput
, etc.)
Do not use result formatters (nested task formatter
, especially the usefile
option)
Due to frequent class loading and thread executions, the following JVM options are recommended, in particular for large projects:
-XX:ReservedCodeCacheSize=256M
-XX:MaxPermSize=1G