crytic
Crytic, pronounced /ˈkrɪtɪk/, is a mutation testing framework for the crystal programming language. Mutation testing is a type of software testing where specific statements in the code are changed to determine if test cases find this defect.
Crytic is in a very early state of development. It is not very clever, making it slow as well.
See CHANGELOG.md for changes between releases.
Blog posts
Introducing crytic - mutation testing in crystal-lang
Installation
Add this to your application's shard.yml
:
development_dependencies:
crytic:
github: hanneskaeufler/crytic
version: ~> 3.0.1
After shards install
, this will place the crytic
executable into the bin/
folder inside your project.
Usage
Running crytic without any arguments will mutate all of the source files found by src/**/*.cr
and use the test-suite containing spec/**/*_spec.cr
. Depending on the size of your project and the duration of a full crystal spec
, this might take quite a bit of time.
./bin/crytic
Crytic can also be run to only mutate statements in one file, let's call that our subject, or --subject
in the command line interface. You can also provide a list of test files to be executed in order to find the defects. This might be helpful to exclude certain long-running integration specs in order to speed up the test suite.
./bin/crytic --subject src/blog/pages/archive.cr spec/blog_spec.cr spec/blog/pages/archive_spec.cr
The above command determines a list of mutations that can be performed on the source code of archive.cr
and joins the blog_spec.cr
and archive_spec.cr
as a test-suite to find suriving mutants.
CLI options
--subject
/-s
specifies a relative filepath to the sourcecode being mutated.
--min-msi
/-m
specifies a threshold as to when to exit the program with 0 even when mutants survived. MSI is the Mutation Score Indicator.
The rest of the unnamed positional arguments are relative filepaths to the specs to be run.
How to read the output
✅ Original test suite passed.
❌ AndOrSwap
The following change didn't fail the test-suite:
@@ -26,7 +26,7 @@
end
end
def ==(other : Chunk)
- ((type == other.type) && (range_a == other.range_a)) && (range_b == other.range_b)
+ ((type == other.type) && (range_a == other.range_a)) || (range_b == other.range_b)
end
end
enum Type
❌ AndOrSwap
The following change didn't fail the test-suite:
@@ -26,7 +26,7 @@
end
end
def ==(other : Chunk)
- ((type == other.type) && (range_a == other.range_a)) && (range_b == other.range_b)
+ ((type == other.type) && (range_a == other.range_a)) || (range_b == other.range_b)
end
end
enum Type
✅ AndOrSwap at line 109, column 13
Finished in 14:02 minutes:
138 mutations, 85 covered, 36 uncovered, 0 errored, 17 timeout. Mutation Score Indicator (MSI): 73.91%
The first good message here is that the Original test suite passed
. Crytic ran crystal spec [with all spec files]
and that exited with exit code 0
. Any other result on your inital test suite and it would not have made sense to continue. Intentionally breaking source code which is already broken is of no use.
Each occurance of ✅
shows that a mutant has been killed, ergo that the change in the source code was detected by the test suite. The line and column numbers are printed to follow the progress through the subject file.
❌ AndOrSwap
is signaling that indeed a mutation was not detected. The diff below shows the change that was made which was not caught by the test suite.
Mutation Badge
To show a badge about your mutation testing efforts like at the top of this readme you can make use of the dashboard of stryker by letting crytic post the msi score to the stryker api. To do that, make sure to have the following env vars set:
CIRCLE_BRANCH => "master",
CIRCLE_PROJECT_REPONAME => "crytic",
CIRCLE_PROJECT_USERNAME => "hanneskaeufler",
STRYKER_DASHBOARD_API_KEY => "apikey",
It is currently limited to work with Circle CI and assumes your project is hosted on GitHub.
Available mutants
There are many ways a code-base can be modified to introduce arbitrary failures. Crytic only provides mutators which keep the code compiling (at least in theory).
AndOrSwap
This mutant replaces the &&
operator by the ||
operator. A typical mutation is:
- if cool && nice
+ if cool || nice
BoolLiteralFlip
This mutant flips literal occurances of true
or false
. A typical mutation is:
def valid
- return true
+ return false
end
ConditionFlip
This mutant flips the if
and else
branch in conditions. It will create an else
branch even if there is none. A typical mutation is:
if true
+ else
doSomething()
end
NumberLiteralChange
This mutation changes literal occurances of numbers by replacing them with "0". "0" gets replaces by "1". A typical mutation is:
- 0
+ 1
NumberLiteralSignChange
This mutation changes the sign of literal numbers. It ignores literal "0". A typical mutation is:
- 5
+ -5
StringLiteralChange
This mutation changes literal occurances of string by appending the string __crytic__
. A typical mutation is:
- "Welcome"
+ "Welcome__crytic__"
AnyAllSwap
This mutation exchanges calls to Enumerable#all? with calls to Enumerable#any? and vice-versa. A typical mutation is:
- [false].all?
+ [false].any?
RegexpLiteralChange
This mutation modifies any regular expression literal to never match anything. A typical mutation is:
- /\d+/
+ /a^/
SelectRejectSwap
This mutation exchanges calls to Enumerable#select with calls to Enumerable#reject and vice-versa. A typical mutation is:
- [1].select(&.nil?)
+ [1].reject(&.nil?)
Credits & inspiration
I have to credit the crystal code-coverage shard which finally helped me create a working mutation testing tool after one or two failed attempts. I took heavy inspirations from its SourceFile class and actually lifted nearly all the code.
One of the more difficult parts of crytic was the resolving of require
statements. In order to work for most projects, crytic has to resolve those statements identical to the way crystal itself does. I achieved this (for now) by copying a bunch of methods from crystal-lang itself.
In order to avoid dependencies for tiny amounts of savings I rather copied/adapted a bit of code from timeout.cr and crystal-diff.
Obviously I didn't invent mutation testing. While I cannot remember where I have read about it initially, my first recollection is the mutant gem for ruby.
Alternatives
Although not having tested it myself yet, the mull libray is supposed to work for any llvm based language, which I believe crystal is.
Contributing
- Fork it (https://github.com/hanneskaeufler/crytic/fork)
- Create your feature branch (
git checkout -b my-new-feature
) - Run tests locally with
crystal spec
- Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create a new Pull Request
Contributors
- hanneskaeufler Hannes Käufler - creator, maintainer
- anicholson Andy Nicholson - contributor