diff --git a/Cargo.lock b/Cargo.lock
index 26042d3..91b3f2c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -11,6 +11,27 @@ dependencies = [
"memchr",
]
+[[package]]
+name = "anstyle"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87"
+
+[[package]]
+name = "assert_cmd"
+version = "2.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88903cb14723e4d4003335bb7f8a14f27691649105346a0f0957466c096adfe6"
+dependencies = [
+ "anstyle",
+ "bstr",
+ "doc-comment",
+ "predicates",
+ "predicates-core",
+ "predicates-tree",
+ "wait-timeout",
+]
+
[[package]]
name = "autocfg"
version = "1.1.0"
@@ -23,6 +44,17 @@ version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+[[package]]
+name = "bstr"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "542f33a8835a0884b006a0c3df3dadd99c0c3f296ed26c2fdc8028e01ad6230c"
+dependencies = [
+ "memchr",
+ "regex-automata",
+ "serde",
+]
+
[[package]]
name = "cc"
version = "1.0.79"
@@ -172,6 +204,18 @@ dependencies = [
"memchr",
]
+[[package]]
+name = "difflib"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
+
+[[package]]
+name = "doc-comment"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
+
[[package]]
name = "either"
version = "1.8.1"
@@ -205,6 +249,15 @@ dependencies = [
"libc",
]
+[[package]]
+name = "float-cmp"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4"
+dependencies = [
+ "num-traits",
+]
+
[[package]]
name = "getrandom"
version = "0.2.8"
@@ -277,6 +330,15 @@ dependencies = [
"windows-sys 0.45.0",
]
+[[package]]
+name = "itertools"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57"
+dependencies = [
+ "either",
+]
+
[[package]]
name = "itoa"
version = "1.0.5"
@@ -287,11 +349,14 @@ checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440"
name = "kode-kraken"
version = "0.1.0"
dependencies = [
+ "assert_cmd",
"clap",
"cli-table",
"csv",
"horrorshow",
"indicatif",
+ "num-traits",
+ "predicates",
"rand",
"rayon",
"regex",
@@ -314,9 +379,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
-version = "0.2.139"
+version = "0.2.150"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79"
+checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c"
[[package]]
name = "linux-raw-sys"
@@ -335,9 +400,9 @@ dependencies = [
[[package]]
name = "memchr"
-version = "2.5.0"
+version = "2.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
+checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167"
[[package]]
name = "memoffset"
@@ -348,6 +413,12 @@ dependencies = [
"autocfg",
]
+[[package]]
+name = "normalize-line-endings"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be"
+
[[package]]
name = "nu-ansi-term"
version = "0.46.0"
@@ -358,6 +429,15 @@ dependencies = [
"winapi",
]
+[[package]]
+name = "num-traits"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c"
+dependencies = [
+ "autocfg",
+]
+
[[package]]
name = "num_cpus"
version = "1.15.0"
@@ -410,6 +490,37 @@ version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
+[[package]]
+name = "predicates"
+version = "3.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dfc28575c2e3f19cb3c73b93af36460ae898d426eba6fc15b9bd2a5220758a0"
+dependencies = [
+ "anstyle",
+ "difflib",
+ "float-cmp",
+ "itertools",
+ "normalize-line-endings",
+ "predicates-core",
+ "regex",
+]
+
+[[package]]
+name = "predicates-core"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174"
+
+[[package]]
+name = "predicates-tree"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf"
+dependencies = [
+ "predicates-core",
+ "termtree",
+]
+
[[package]]
name = "proc-macro-error"
version = "1.0.4"
@@ -515,6 +626,12 @@ dependencies = [
"regex-syntax",
]
+[[package]]
+name = "regex-automata"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f"
+
[[package]]
name = "regex-syntax"
version = "0.7.1"
@@ -619,6 +736,12 @@ dependencies = [
"winapi-util",
]
+[[package]]
+name = "termtree"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76"
+
[[package]]
name = "thread_local"
version = "1.1.7"
diff --git a/Cargo.toml b/Cargo.toml
index 47448a2..7827823 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -13,7 +13,7 @@ path = "src/lib.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
-clap = { version = "4.1.6", features = ["derive"] }
+clap = { version = "4.1.6", features = ["derive"] }
cli-table = "0.4.7"
csv = "1.2.1"
indicatif = "0.17.3"
@@ -30,3 +30,6 @@ wait-timeout = "0.2.0"
regex = "1.8.1"
tracing-appender = "0.2.2"
horrorshow = "0.8.4"
+num-traits = "0.2.17"
+assert_cmd = "2.0.12"
+predicates = "3.0.4"
diff --git a/assets/about.txt b/assets/about.txt
index f94705d..b2a9b4f 100644
--- a/assets/about.txt
+++ b/assets/about.txt
@@ -1,42 +1,44 @@
-###########################################################################################################
+#####################################################################################################################################
+
+ /$$ /$$ /$$ /$$ /$$ /$$ /$$
+| $$$ /$$$ | $$ | $$ | $$ /$$/ | $$
+| $$$$ /$$$$ /$$ /$$ /$$$$$$ /$$$$$$ /$$$$$$$ /$$$$$$ | $$ /$$/ /$$$$$$ /$$$$$$ | $$ /$$ /$$$$$$ /$$$$$$$
+| $$ $$/$$ $$| $$ | $$|_ $$_/ |____ $$| $$__ $$|_ $$_/ | $$$$$/ /$$__ $$|____ $$| $$ /$$/ /$$__ $$| $$__ $$
+| $$ $$$| $$| $$ | $$ | $$ /$$$$$$$| $$ \ $$ | $$ | $$ $$ | $$ \__/ /$$$$$$$| $$$$$$/ | $$$$$$$$| $$ \ $$
+| $$\ $ | $$| $$ | $$ | $$ /$$ /$$__ $$| $$ | $$ | $$ /$$ | $$\ $$ | $$ /$$__ $$| $$_ $$ | $$_____/| $$ | $$
+| $$ \/ | $$| $$$$$$/ | $$$$/| $$$$$$$| $$ | $$ | $$$$/ | $$ \ $$| $$ | $$$$$$$| $$ \ $$| $$$$$$$| $$ | $$
+|__/ |__/ \______/ \___/ \_______/|__/ |__/ \___/ |__/ \__/|__/ \_______/|__/ \__/ \_______/|__/ |__/
- /$$ /$$ /$$ /$$ /$$ /$$
-| $$ /$$/ | $$ | $$ /$$/ | $$
-| $$ /$$/ /$$$$$$ /$$$$$$$ /$$$$$$ | $$ /$$/ /$$$$$$ /$$$$$$ | $$ /$$ /$$$$$$ /$$$$$$$
-| $$$$$/ /$$__ $$ /$$__ $$ /$$__ $$ | $$$$$/ /$$__ $$|____ $$| $$ /$$/ /$$__ $$| $$__ $$
-| $$ $$ | $$ \ $$| $$ | $$| $$$$$$$$ | $$ $$ | $$ \__/ /$$$$$$$| $$$$$$/ | $$$$$$$$| $$ \ $$
-| $$\ $$ | $$ | $$| $$ | $$| $$_____/ | $$\ $$ | $$ /$$__ $$| $$_ $$ | $$_____/| $$ | $$
-| $$ \ $$| $$$$$$/| $$$$$$$| $$$$$$$ | $$ \ $$| $$ | $$$$$$$| $$ \ $$| $$$$$$$| $$ | $$
-|__/ \__/ \______/ \_______/ \_______/ |__/ \__/|__/ \_______/|__/ \__/ \_______/|__/ |__/
- ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⣀⣀⣀⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
- ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣶⣿⣿⣿⣿⣿⣿⣿⣿⣷⣦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
- ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣀⠀⠀⠀⢠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
- ⠀⠀⠀⠀⠀⠀⠀⠀⢠⣾⡿⠿⢿⣿⣷⣠⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡆⣷⣠⣴⣶⣶⣤⡀⠀⠀⠀⠀⠀⠀⠀⠀
- ⠀⠀⠀⠀⠀⠀⠀⠀⠻⢿⡄⠀⠀⠹⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣧⣿⣿⣟⠉⢹⣿⣷⠀⠀⠀⠀⠀⠀⠀⠀
- ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠛⠿⠿⠿⠋⠀⠀⠀⠀⠀⠀⠀⠀
- ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣾⣿⡿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠉⣿⣿⣧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
- ⠀⠀⠀⢀⣀⣀⠀⠀⠀⠀⠀⣰⣿⣿⡟⠁⠘⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠃⠀⠈⢿⣿⣷⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
- ⠀⣴⣾⣿⣿⣿⣿⣶⡀⢀⣾⣿⣿⠋⠀⠀⠀⠈⠻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠃⠀⠀⠀⠀⠹⣿⣿⣦⠀⠀⠀⠀⠀⠀⠀⠀⠀
- ⢸⣿⡁⠀⠀⢀⣿⣿⢇⣾⣿⣿⠃⠀⠀⠀⠀⠀⠀⣿⡈⠙⢿⣿⣿⣿⠿⠋⢩⡇⠀⠀⠀⠀⠀⠀⠙⣿⣿⣇⠀⠀⠀⠀⠀⠀⠀⠀
- ⠈⠛⠛⣠⣴⣿⡿⠋⢸⣿⣿⡇⠀⠀⠀⠀⠀⠀⠀⣿⣿⣶⣾⣿⣿⣿⣷⣶⣿⡇⠀⠀⠀⠀⠀⠀⠀⣻⣿⣿⠀⠀⠀⠀⠀⠀⠀⠀
- ⠀⣠⣾⣿⡿⠋⠀⠀⢻⣿⣿⣷⡀⠀⠀⠀⠀⣠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⡀⠀⠀⠀⠀⢠⣿⣿⣏⣠⣤⣶⣤⠀⠀⠀⠀
- ⢰⣿⣿⣟⠀⠀⠀⠀⠘⢿⣿⣿⣿⣷⣶⣶⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⣤⣤⣴⣿⣿⣿⣿⠋⠀⠀⠀⠀⠀⠀⠀
- ⢸⣿⣿⣿⣦⣄⣀⠀⠀⠀⠉⠙⠛⠛⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠿⠛⠉⢻⣿⣄⠀⠀⠀⠀⠀⠀⠀
- ⠀⠙⠿⣿⣿⣿⣿⣿⣿⣿⣶⣶⣶⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⡀⠀⠀⠀⠈⢿⣿⣶⣄⠀⠀⠀⠀⠀
- ⠀⠀⠀⠀⠈⠉⠉⠙⠛⠛⠛⠛⠛⣿⣿⣿⣿⠟⢋⣿⣿⣿⡿⠋⠙⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣆⠀⠀⠀⠀⠙⢿⣿⣧⡀⠀⠀⠀
- ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣾⣿⣿⠟⠁⠀⣿⣿⣿⠟⠀⠀⢀⣿⣿⣿⡿⢿⣿⣿⣿⣿⣿⣿⣆⠀⠀⠀⠀⠈⢿⣿⣷⠀⠀⠀
- ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣾⣿⣿⠏⠀⠀⢸⣿⣿⣿⠀⠀⠀⢸⣿⣿⣿⠀⠈⢻⣿⣿⣿⢿⣿⣿⣦⡀⠀⠀⠀⣸⣿⣿⠀⣀⡄
- ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣰⣿⣿⡟⠀⠀⠀⠸⣿⣿⣿⠀⠀⠀⢻⣿⣿⣿⠀⠀⠀⢻⣿⣿⡆⠹⢿⣿⣿⣶⣶⣾⣿⣿⣿⣿⠋⠀
- ⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⣿⡿⠁⠀⠀⠀⠀⢿⣿⣿⡆⠀⠀⠸⣿⣿⣿⡄⠀⠀⠀⢿⣿⣿⠀⠀⠙⠛⠿⠿⠿⠛⠋⢸⣿⠀⠀
- ⠀⠀⠀⠀⠀⠀⣠⣴⣿⣿⡿⠛⠁⠀⠀⠀⠀⠀⠘⣿⣿⣿⠀⠀⠀⣿⣿⣿⡇⠀⠀⠀⢸⣿⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⠀⠀
- ⠀⠀⠀⢠⣶⣿⣿⠿⠋⠁⠒⠛⢻⣷⠀⠀⠀⠀⠀⢹⣿⣿⡇⠀⣠⣿⣿⣿⢃⣴⣿⠟⠛⢿⣿⣿⡄⠀⠀⠀⠀⠀⠀⢠⣿⣿⠀⠀
- ⠀⠀⢰⣿⣿⠟⠁⠀⠀⠀⠀⢀⣾⡟⠀⠀⠀⠀⠀⠘⣿⣿⣧⣾⣿⣿⠟⠁⣾⣿⡇⠀⠀⠘⢿⣿⣿⣦⡀⠀⠀⣀⣴⣿⣿⠃⠀⠀
- ⠀⠀⣿⣿⡇⠀⠀⢀⡄⠀⢠⣿⣿⠀⠀⠀⠀⠀⠀⢰⣿⣿⣿⣿⠟⠁⠀⠀⢿⣿⣇⠀⠀⠀⠈⠻⣿⣿⣿⣿⣿⣿⡿⠟⠁⠀⠀⠀
- ⠀⠀⠹⣿⣷⣄⣀⣼⡇⠀⢸⣿⣿⡀⠀⠀⠀⠀⣠⣿⣿⣿⡿⠋⠀⠀⠀⠀⢸⣿⣿⡀⠀⠀⠀⠀⠀⠉⠉⠉⠉⠁⠀⠀⠀⠀⠀⠀
- ⠀⠀⠀⠈⠛⠛⠛⠋⠀⠀⠀⢻⣿⣿⣶⣶⣶⣿⣿⣿⣿⣿⠁⠀⠀⠀⠀⠀⠀⣿⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
- ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠛⠛⠛⠛⠛⠉⣿⣿⣿⡀⠀⠀⠀⠀⠀⠀⣿⣿⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
- ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠸⣿⣿⣷⣄⣀⠀⢀⣀⣴⣿⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
- ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⢿⣿⣿⣿⣿⣿⣿⣿⡿⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
- ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠉⠉⠉⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
-###########################################################################################################
\ No newline at end of file
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⣀⣀⣀⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣶⣿⣿⣿⣿⣿⣿⣿⣿⣷⣦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣀⠀⠀⠀⢠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+ ⠀⠀⠀⠀⠀⠀⠀⠀⢠⣾⡿⠿⢿⣿⣷⣠⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡆⣷⣠⣴⣶⣶⣤⡀⠀⠀⠀⠀⠀⠀⠀⠀
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠻⢿⡄⠀⠀⠹⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣧⣿⣿⣟⠉⢹⣿⣷⠀⠀⠀⠀⠀⠀⠀⠀
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠛⠿⠿⠿⠋⠀⠀⠀⠀⠀⠀⠀⠀
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣾⣿⡿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠉⣿⣿⣧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+ ⠀⠀⠀⢀⣀⣀⠀⠀⠀⠀⠀⣰⣿⣿⡟⠁⠘⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠃⠀⠈⢿⣿⣷⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+ ⠀⣴⣾⣿⣿⣿⣿⣶⡀⢀⣾⣿⣿⠋⠀⠀⠀⠈⠻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠃⠀⠀⠀⠀⠹⣿⣿⣦⠀⠀⠀⠀⠀⠀⠀⠀⠀
+ ⢸⣿⡁⠀⠀⢀⣿⣿⢇⣾⣿⣿⠃⠀⠀⠀⠀⠀⠀⣿⡈⠙⢿⣿⣿⣿⠿⠋⢩⡇⠀⠀⠀⠀⠀⠀⠙⣿⣿⣇⠀⠀⠀⠀⠀⠀⠀⠀
+ ⠈⠛⠛⣠⣴⣿⡿⠋⢸⣿⣿⡇⠀⠀⠀⠀⠀⠀⠀⣿⣿⣶⣾⣿⣿⣿⣷⣶⣿⡇⠀⠀⠀⠀⠀⠀⠀⣻⣿⣿⠀⠀⠀⠀⠀⠀⠀⠀
+ ⠀⣠⣾⣿⡿⠋⠀⠀⢻⣿⣿⣷⡀⠀⠀⠀⠀⣠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⡀⠀⠀⠀⠀⢠⣿⣿⣏⣠⣤⣶⣤⠀⠀⠀⠀
+ ⢰⣿⣿⣟⠀⠀⠀⠀⠘⢿⣿⣿⣿⣷⣶⣶⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⣤⣤⣴⣿⣿⣿⣿⠋⠀⠀⠀⠀⠀⠀⠀
+ ⢸⣿⣿⣿⣦⣄⣀⠀⠀⠀⠉⠙⠛⠛⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠿⠛⠉⢻⣿⣄⠀⠀⠀⠀⠀⠀⠀
+ ⠀⠙⠿⣿⣿⣿⣿⣿⣿⣿⣶⣶⣶⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⡀⠀⠀⠀⠈⢿⣿⣶⣄⠀⠀⠀⠀⠀
+ ⠀⠀⠀⠀⠈⠉⠉⠙⠛⠛⠛⠛⠛⣿⣿⣿⣿⠟⢋⣿⣿⣿⡿⠋⠙⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣆⠀⠀⠀⠀⠙⢿⣿⣧⡀⠀⠀⠀
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣾⣿⣿⠟⠁⠀⣿⣿⣿⠟⠀⠀⢀⣿⣿⣿⡿⢿⣿⣿⣿⣿⣿⣿⣆⠀⠀⠀⠀⠈⢿⣿⣷⠀⠀⠀
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣾⣿⣿⠏⠀⠀⢸⣿⣿⣿⠀⠀⠀⢸⣿⣿⣿⠀⠈⢻⣿⣿⣿⢿⣿⣿⣦⡀⠀⠀⠀⣸⣿⣿⠀⣀⡄
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣰⣿⣿⡟⠀⠀⠀⠸⣿⣿⣿⠀⠀⠀⢻⣿⣿⣿⠀⠀⠀⢻⣿⣿⡆⠹⢿⣿⣿⣶⣶⣾⣿⣿⣿⣿⠋⠀
+ ⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⣿⡿⠁⠀⠀⠀⠀⢿⣿⣿⡆⠀⠀⠸⣿⣿⣿⡄⠀⠀⠀⢿⣿⣿⠀⠀⠙⠛⠿⠿⠿⠛⠋⢸⣿⠀⠀
+ ⠀⠀⠀⠀⠀⠀⣠⣴⣿⣿⡿⠛⠁⠀⠀⠀⠀⠀⠘⣿⣿⣿⠀⠀⠀⣿⣿⣿⡇⠀⠀⠀⢸⣿⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⠀⠀
+ ⠀⠀⠀⢠⣶⣿⣿⠿⠋⠁⠒⠛⢻⣷⠀⠀⠀⠀⠀⢹⣿⣿⡇⠀⣠⣿⣿⣿⢃⣴⣿⠟⠛⢿⣿⣿⡄⠀⠀⠀⠀⠀⠀⢠⣿⣿⠀⠀
+ ⠀⠀⢰⣿⣿⠟⠁⠀⠀⠀⠀⢀⣾⡟⠀⠀⠀⠀⠀⠘⣿⣿⣧⣾⣿⣿⠟⠁⣾⣿⡇⠀⠀⠘⢿⣿⣿⣦⡀⠀⠀⣀⣴⣿⣿⠃⠀⠀
+ ⠀⠀⣿⣿⡇⠀⠀⢀⡄⠀⢠⣿⣿⠀⠀⠀⠀⠀⠀⢰⣿⣿⣿⣿⠟⠁⠀⠀⢿⣿⣇⠀⠀⠀⠈⠻⣿⣿⣿⣿⣿⣿⡿⠟⠁⠀⠀⠀
+ ⠀⠀⠹⣿⣷⣄⣀⣼⡇⠀⢸⣿⣿⡀⠀⠀⠀⠀⣠⣿⣿⣿⡿⠋⠀⠀⠀⠀⢸⣿⣿⡀⠀⠀⠀⠀⠀⠉⠉⠉⠉⠁⠀⠀⠀⠀⠀⠀
+ ⠀⠀⠀⠈⠛⠛⠛⠋⠀⠀⠀⢻⣿⣿⣶⣶⣶⣿⣿⣿⣿⣿⠁⠀⠀⠀⠀⠀⠀⣿⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠛⠛⠛⠛⠛⠉⣿⣿⣿⡀⠀⠀⠀⠀⠀⠀⣿⣿⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠸⣿⣿⣷⣄⣀⠀⢀⣀⣴⣿⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⢿⣿⣿⣿⣿⣿⣿⣿⡿⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠉⠉⠉⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+
+#####################################################################################################################################
\ No newline at end of file
diff --git a/kodekraken.config.json b/kodekraken.config.json
index a89da3b..2d32524 100644
--- a/kodekraken.config.json
+++ b/kodekraken.config.json
@@ -1,24 +1,28 @@
{
- "ignore": {
- "ignore_files": [
- "^.*Test\\.[^.]*$"
- ],
- "ignore_directories": [
- "dist",
- "build",
- "bin",
- ".gradle",
- ".idea",
- "gradle"
- ]
- },
- "threading": {
- "max_threads": 30
- },
- "output": {
- "display_end_table": false
- },
- "logging": {
- "log_level": "DEBUG"
- }
+ "general": {
+ "timeout": 10,
+ "operators": []
+ },
+ "ignore": {
+ "ignore_files": [
+ "^.*Test\\.[^.]*$"
+ ],
+ "ignore_directories": [
+ "dist",
+ "build",
+ "bin",
+ ".gradle",
+ ".idea",
+ "gradle"
+ ]
+ },
+ "threading": {
+ "max_threads": 30
+ },
+ "output": {
+ "display_end_table": false
+ },
+ "logging": {
+ "log_level": "INFO"
+ }
}
diff --git a/kotlin-test-projects/demo/kodekraken.config.json b/kotlin-test-projects/demo/kodekraken.config.json
deleted file mode 100644
index 111aa79..0000000
--- a/kotlin-test-projects/demo/kodekraken.config.json
+++ /dev/null
@@ -1,27 +0,0 @@
-{
- "ignore": {
- "ignore_files": [
- "^.*Test\\.[^.]*$"
- ],
- "ignore_directories": [
- "dist",
- "build",
- "bin",
- ".gradle",
- ".idea",
- "gradle"
- ]
- },
- "threading": {
- "max_threads": 30
- },
- "output": {
- "display_end_table": false
- },
- "logging": {
- "log_level": "DEBUG"
- },
- "general": {
- "timeout": 60
- }
-}
diff --git a/report.html b/report.html
new file mode 100644
index 0000000..43ba2bb
--- /dev/null
+++ b/report.html
@@ -0,0 +1,10 @@
+
Kode Kraken ResultsKode Kraken Results |
---|
File Name | # of Mutations | # Survived | # Killed | Score |
file2 | 2 | 0 | 0 | NaN% |
file1 | 2 | 0 | 0 | NaN% |
\ No newline at end of file
diff --git a/src/cli.rs b/src/cli.rs
index 69a3645..9807cd9 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -1,11 +1,12 @@
use std::{path::Path, time::Duration};
use clap::{Args, CommandFactory, Parser, Subcommand};
+use tracing_appender::non_blocking::WorkerGuard;
use crate::{
config::KodeKrakenConfig,
error::{self, KodeKrakenError},
- mutation_tool::{MutationToolBuilder, OUT_DIRECTORY},
+ mutation_tool::MutationToolBuilder,
};
#[derive(Subcommand, Debug, Clone)]
@@ -20,7 +21,7 @@ pub enum Commands {
/// Clean the kode-kraken-dist directory
/// This will delete all files in the directory
/// This is useful if you want to remove all the files
- Clean,
+ Clean(MutationCommandConfig),
}
const ABOUT: &str = include_str!("../assets/about.txt");
@@ -37,7 +38,7 @@ pub struct Cli {
pub command: Commands,
}
-#[derive(Args, Debug, Clone)]
+#[derive(Args, Debug, Clone, PartialEq, Eq)]
pub struct MutationCommandConfig {
/// The path to the files to be mutated
/// Error will be thrown if the path is not a directory
@@ -86,21 +87,25 @@ where
}
}
-pub fn run_cli(config: KodeKrakenConfig) {
+pub fn run_cli() {
+ let _guard: WorkerGuard;
+ tracing::info!("Starting Kode Kraken");
+
let args = Cli::parse();
let mutate_tool_builder = MutationToolBuilder::new();
match args.command {
Commands::Mutate(mutate_config) => {
+ let config = KodeKrakenConfig::load_config(mutate_config.path.clone());
+ _guard = setup_logging(&config.logging.log_level, mutate_config.path.clone());
+
let mut tool = mutate_tool_builder
.set_mutate_config(mutate_config)
.set_general_config(config)
.set_mutation_comment(true)
.build();
- println!("{:#?}", tool.kodekraken_config.general.timeout);
let res = match tool.kodekraken_config.general.timeout {
Some(timeout) => {
- println!("Timeout set to {} seconds", timeout);
run_with_timeout(move || tool.mutate(), Duration::from_secs(timeout))
}
None => tool.mutate(),
@@ -145,9 +150,9 @@ pub fn run_cli(config: KodeKrakenConfig) {
println!("3. Edit the config file to your liking");
}
}
- Commands::Clean => {
+ Commands::Clean(config) => {
// Check to see if the output directory exists
- let output_dir = Path::new(OUT_DIRECTORY);
+ let output_dir = Path::new(config.path.as_str()).join("kode-kraken-dist");
if output_dir.exists() {
// Delete the output directory
std::fs::remove_dir_all(output_dir).expect("Could not delete output directory");
@@ -156,6 +161,32 @@ pub fn run_cli(config: KodeKrakenConfig) {
}
}
+fn setup_logging(log_level: &str, dir: String) -> WorkerGuard {
+ let log_level = match log_level.to_lowercase().as_str() {
+ "trace" => tracing::Level::TRACE,
+ "debug" => tracing::Level::DEBUG,
+ "info" => tracing::Level::INFO,
+ "warn" => tracing::Level::WARN,
+ "error" => tracing::Level::ERROR,
+ _ => tracing::Level::INFO,
+ };
+ // Create dist log folder if it doesn't exist
+ let log_dir = Path::new(dir.as_str())
+ .join("kode-kraken-dist")
+ .join("logs");
+ std::fs::create_dir_all(&log_dir).expect("Could not create log directory");
+ let file_appender = tracing_appender::rolling::never(log_dir, "kode-kraken.log");
+ let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
+ tracing_subscriber::fmt()
+ .with_max_level(log_level)
+ .with_ansi(false)
+ .with_target(false)
+ .with_writer(non_blocking)
+ .with_thread_ids(true)
+ .init();
+ guard
+}
+
#[cfg(test)]
mod tests {
use super::*;
diff --git a/src/config.rs b/src/config.rs
index f0e2b5d..06658b9 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -1,8 +1,10 @@
-use std::{fs, io::BufReader};
+use std::{fs, io::BufReader, path::Path};
-use serde::Deserialize;
+use serde::{Deserialize, Serialize};
-#[derive(Debug, Deserialize, Default)]
+use crate::mutation_tool::MutationOperators;
+
+#[derive(Debug, Serialize, Deserialize, Default, PartialEq, Eq, Clone)]
pub struct KodeKrakenConfig {
pub general: GeneralConfig,
pub ignore: IgnoreConfig,
@@ -11,37 +13,55 @@ pub struct KodeKrakenConfig {
pub logging: LoggingConfig,
}
-#[derive(Debug, Deserialize)]
+#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
pub struct GeneralConfig {
/// The time in seconds to wait for the mutation tool to finish
/// before killing the process
pub timeout: Option,
+
+ #[serde(default)]
+ pub operators: Vec,
}
impl Default for GeneralConfig {
/// Set Default timeout of 5 minutes
fn default() -> Self {
- Self { timeout: None }
+ use MutationOperators::*;
+ Self {
+ timeout: None,
+ operators: vec![
+ ArithmeticReplacementOperator,
+ UnaryRemovalOperator,
+ LogicalReplacementOperator,
+ RelationalReplacementOperator,
+ AssignmentReplacementOperator,
+ UnaryReplacementOperator,
+ NotNullAssertionOperator,
+ ElvisRemoveOperator,
+ ElvisLiteralChangeOperator,
+ LiteralChangeOpeator,
+ ],
+ }
}
}
-#[derive(Debug, Deserialize)]
+#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
pub struct LoggingConfig {
pub log_level: String,
}
-#[derive(Debug, Deserialize)]
+#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
pub struct IgnoreConfig {
pub ignore_files: Vec,
pub ignore_directories: Vec,
}
-#[derive(Debug, Deserialize)]
+#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
pub struct ThreadingConfig {
pub max_threads: usize,
}
-#[derive(Debug, Deserialize, Default)]
+#[derive(Debug, Serialize, Deserialize, Default, PartialEq, Eq, Clone)]
pub struct OutputConfig {
pub display_end_table: bool,
}
@@ -51,8 +71,8 @@ impl KodeKrakenConfig {
Self::default()
}
- pub fn load_config() -> Self {
- match fs::File::open("kodekraken.config.json") {
+ pub fn load_config>(path: P) -> Self {
+ let mut config = match fs::File::open(path.as_ref().join("kodekraken.config.json")) {
Ok(file) => {
let buffer_reader = BufReader::new(file);
match serde_json::from_reader(buffer_reader) {
@@ -68,10 +88,17 @@ impl KodeKrakenConfig {
}
}
Err(_) => {
- println!("[WARNING] ⚠️ Could not find kodekraken.config.json file, using default config.");
+ println!("[WARNING] ⚠️ Could not find kodekraken.config.json file in root directory, using default config.");
Self::default()
}
+ };
+
+ if config.general.operators.is_empty() {
+ println!("[WARNING] ⚠️ No mutation operators specified in config file, using default operators.");
+ config.general.operators = GeneralConfig::default().operators;
}
+
+ config
}
}
@@ -106,3 +133,194 @@ impl Default for LoggingConfig {
}
}
}
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use std::{env::temp_dir, fs::File, io::Write};
+
+ #[test]
+ fn test_default_general_config() {
+ let default_general = GeneralConfig::default();
+ assert_eq!(default_general.timeout, None);
+ assert_eq!(
+ default_general.operators,
+ vec![
+ MutationOperators::ArithmeticReplacementOperator,
+ MutationOperators::UnaryRemovalOperator,
+ MutationOperators::LogicalReplacementOperator,
+ MutationOperators::RelationalReplacementOperator,
+ MutationOperators::AssignmentReplacementOperator,
+ MutationOperators::UnaryReplacementOperator,
+ MutationOperators::NotNullAssertionOperator,
+ MutationOperators::ElvisRemoveOperator,
+ MutationOperators::ElvisLiteralChangeOperator,
+ MutationOperators::LiteralChangeOpeator,
+ ]
+ );
+ }
+
+ #[test]
+ fn test_default_ignore_config() {
+ let default_ignore = IgnoreConfig::default();
+ assert_eq!(default_ignore.ignore_files.len(), 1);
+ assert_eq!(default_ignore.ignore_directories.len(), 6);
+ }
+
+ #[test]
+ fn test_default_threading_config() {
+ let default_threading = ThreadingConfig::default();
+ assert_eq!(default_threading.max_threads, 30);
+ }
+
+ #[test]
+ fn test_default_logging_config() {
+ let default_logging = LoggingConfig::default();
+ assert_eq!(default_logging.log_level, "info");
+ }
+
+ #[test]
+ fn test_default_output_config() {
+ let default_output = OutputConfig::default();
+ assert_eq!(default_output.display_end_table, false);
+ }
+
+ #[test]
+ fn test_new_kodekraken_config() {
+ let config = KodeKrakenConfig::new();
+ assert_eq!(config.general.timeout, None);
+ assert_eq!(
+ config.general.operators,
+ vec![
+ MutationOperators::ArithmeticReplacementOperator,
+ MutationOperators::UnaryRemovalOperator,
+ MutationOperators::LogicalReplacementOperator,
+ MutationOperators::RelationalReplacementOperator,
+ MutationOperators::AssignmentReplacementOperator,
+ MutationOperators::UnaryReplacementOperator,
+ MutationOperators::NotNullAssertionOperator,
+ MutationOperators::ElvisRemoveOperator,
+ MutationOperators::ElvisLiteralChangeOperator,
+ MutationOperators::LiteralChangeOpeator,
+ ]
+ );
+ assert_eq!(config.ignore.ignore_files.len(), 1);
+ assert_eq!(config.ignore.ignore_directories.len(), 6);
+ assert_eq!(config.threading.max_threads, 30);
+ assert_eq!(config.output.display_end_table, false);
+ assert_eq!(config.logging.log_level, "info");
+ }
+
+ #[test]
+ fn test_load_config_from_valid_file() {
+ let temp_dir = temp_dir();
+ let file_path = temp_dir.join("kodekraken.config.json");
+ let mut file = File::create(file_path).expect("Failed to create temporary file");
+
+ // Create a valid JSON content
+ let config = KodeKrakenConfig {
+ general: GeneralConfig {
+ timeout: Some(10),
+ operators: vec![
+ MutationOperators::UnaryRemovalOperator,
+ MutationOperators::AssignmentReplacementOperator,
+ ],
+ },
+ ignore: IgnoreConfig {
+ ignore_files: vec!["file1".into(), "file2".into()],
+ ignore_directories: vec!["dir1".into(), "dir2".into()],
+ },
+ threading: ThreadingConfig { max_threads: 42 },
+ output: OutputConfig {
+ display_end_table: true,
+ },
+ logging: LoggingConfig {
+ log_level: "debug".into(),
+ },
+ };
+
+ let config_json = serde_json::to_string_pretty(&config).unwrap();
+ file.write_all(config_json.as_bytes())
+ .expect("Failed to write to temporary file");
+
+ let config = KodeKrakenConfig::load_config(temp_dir);
+ assert_eq!(config.general.timeout, Some(10));
+ assert_eq!(
+ config.general.operators,
+ vec![
+ MutationOperators::UnaryRemovalOperator,
+ MutationOperators::AssignmentReplacementOperator
+ ]
+ );
+ assert_eq!(config.ignore.ignore_files, vec!["file1", "file2"]);
+ assert_eq!(config.ignore.ignore_directories, vec!["dir1", "dir2"]);
+ assert_eq!(config.threading.max_threads, 42);
+ assert_eq!(config.output.display_end_table, true);
+ assert_eq!(config.logging.log_level, "debug");
+ }
+
+ #[test]
+ fn test_load_config_from_invalid_file() {
+ // Create an invalid JSON content
+ let temp_dir = temp_dir().join("invalid_config");
+ // Create the temporary directory
+ fs::create_dir_all(&temp_dir).expect("Failed to create temporary directory");
+ // Create the temporary file
+ let file_path = temp_dir.join("kodekraken.config.json");
+ let mut file = File::create(file_path).expect("Failed to create temporary file");
+
+ let invalid_json_content = r#"
+ {
+ "general": {
+ "timeout": "invalid_timeout_value"
+ }
+ }
+ "#;
+
+ writeln!(file, "{}", invalid_json_content).expect("Failed to write to temporary file");
+ let config = KodeKrakenConfig::load_config(temp_dir);
+ // Since the JSON is invalid, it should fall back to default values
+ assert_eq!(config.general.timeout, None);
+ assert_eq!(
+ config.general.operators,
+ vec![
+ MutationOperators::ArithmeticReplacementOperator,
+ MutationOperators::UnaryRemovalOperator,
+ MutationOperators::LogicalReplacementOperator,
+ MutationOperators::RelationalReplacementOperator,
+ MutationOperators::AssignmentReplacementOperator,
+ MutationOperators::UnaryReplacementOperator,
+ MutationOperators::NotNullAssertionOperator,
+ MutationOperators::ElvisRemoveOperator,
+ MutationOperators::ElvisLiteralChangeOperator,
+ MutationOperators::LiteralChangeOpeator,
+ ]
+ );
+ }
+
+ #[test]
+ fn test_load_config_from_missing_file() {
+ // Create a temp directory
+ let temp_dir = temp_dir().join("missing_config");
+ // Create the temp directory
+ fs::create_dir_all(&temp_dir).expect("Failed to create temporary directory");
+ // Test loading config from a missing file
+ let config = KodeKrakenConfig::load_config(temp_dir);
+ // Since the file is missing, it should fall back to default values
+ assert_eq!(config.general.timeout, None);
+ assert_eq!(
+ config.general.operators,
+ vec![
+ MutationOperators::ArithmeticReplacementOperator,
+ MutationOperators::UnaryRemovalOperator,
+ MutationOperators::LogicalReplacementOperator,
+ MutationOperators::RelationalReplacementOperator,
+ MutationOperators::AssignmentReplacementOperator,
+ MutationOperators::UnaryReplacementOperator,
+ MutationOperators::NotNullAssertionOperator,
+ MutationOperators::ElvisRemoveOperator,
+ MutationOperators::ElvisLiteralChangeOperator,
+ MutationOperators::LiteralChangeOpeator,
+ ]
+ );
+ }
+}
diff --git a/src/error.rs b/src/error.rs
index ce4bd86..f576efb 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -2,7 +2,7 @@ use std::io;
pub type Result = std::result::Result;
-#[derive(Debug)]
+#[derive(Debug, PartialEq, Eq, Clone)]
pub enum KodeKrakenError {
FileReadingError(String),
MutationGenerationError,
@@ -42,3 +42,75 @@ impl From for KodeKrakenError {
KodeKrakenError::Error(error.to_string())
}
}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+ use std::io;
+ #[test]
+ fn test_display_file_reading_error() {
+ let error = KodeKrakenError::FileReadingError("Failed to read file".to_string());
+ let expected_output = "Error while reading file: Failed to read file";
+ assert_eq!(error.to_string(), expected_output);
+ }
+
+ #[test]
+ fn test_display_mutation_generation_error() {
+ let error = KodeKrakenError::MutationGenerationError;
+ let expected_output = "Error while generating mutation";
+ assert_eq!(error.to_string(), expected_output);
+ }
+
+ #[test]
+ fn test_display_mutation_gathering_error() {
+ let error = KodeKrakenError::MutationGatheringError;
+ let expected_output = "Error while gathering mutations";
+ assert_eq!(error.to_string(), expected_output);
+ }
+
+ #[test]
+ fn test_display_mutation_build_test_error() {
+ let error = KodeKrakenError::MutationBuildTestError;
+ let expected_output = "Error while building and testing";
+ assert_eq!(error.to_string(), expected_output);
+ }
+
+ #[test]
+ fn test_display_conversion_error() {
+ let error = KodeKrakenError::ConversionError;
+ let expected_output = "Error while converting";
+ assert_eq!(error.to_string(), expected_output);
+ }
+
+ #[test]
+ fn test_display_generic_error() {
+ let error = KodeKrakenError::Error("Something went wrong".to_string());
+ let expected_output = "Error: Something went wrong";
+ assert_eq!(error.to_string(), expected_output);
+ }
+
+ #[test]
+ fn test_from_io_error() {
+ let io_error = io::Error::new(io::ErrorKind::NotFound, "File not found");
+ let converted_error: KodeKrakenError = io_error.into();
+ let expected_error = KodeKrakenError::Error("File not found".to_string());
+ assert_eq!(converted_error, expected_error);
+ }
+
+ #[test]
+ fn test_result_conversion_ok() {
+ let result: Result = Ok(42);
+ assert_eq!(result.is_ok(), true);
+ assert_eq!(result.unwrap(), 42);
+ }
+
+ #[test]
+ fn test_result_conversion_err() {
+ let result: Result = Err(KodeKrakenError::Error("Something went wrong".to_string()));
+ assert_eq!(result.is_err(), true);
+ assert_eq!(
+ result.unwrap_err(),
+ KodeKrakenError::Error("Something went wrong".to_string())
+ );
+ }
+}
diff --git a/src/gradle.rs b/src/gradle.rs
index a6242dd..cb1b4d4 100644
--- a/src/gradle.rs
+++ b/src/gradle.rs
@@ -28,6 +28,50 @@ impl<'a> ToString for GradleCommand<'a> {
}
}
+pub fn build_project_success(path: &PathBuf) -> Result {
+ let res = build_gradle_command(path, GradleCommand::Assemble)?
+ .wait()
+ .map_err(|e| KodeKrakenError::Error(format!("Failed to run gradle command: {}", e)))?;
+ Ok(res.success())
+}
+
+pub fn project_tests_pass(path: &PathBuf) -> Result {
+ let res = build_gradle_command(path, GradleCommand::Test("*"))?
+ .wait()
+ .map_err(|e| KodeKrakenError::Error(format!("Failed to run gradle command: {}", e)))?;
+ Ok(res.success())
+}
+
+/// Checks to see if gradle is installed on the system
+pub fn is_gradle_installed() -> bool {
+ let mut cmd = if cfg!(unix) {
+ Command::new("gradle")
+ } else if cfg!(windows) {
+ Command::new("cmd")
+ } else {
+ panic!("Unsupported OS");
+ };
+ let mut args = vec![];
+ if cfg!(windows) {
+ args.append(&mut ["/C".into(), "gradlew.bat".into()].to_vec())
+ }
+ args.append(&mut ["--version".to_string()].to_vec());
+
+ let res = cmd
+ .args(args)
+ .stderr(Stdio::null())
+ .stdout(Stdio::null())
+ .spawn()
+ .map_err(|e| KodeKrakenError::Error(format!("Failed to run gradle command: {}", e)));
+ match res {
+ Ok(mut child) => {
+ let res = child.wait().unwrap();
+ res.success()
+ }
+ Err(_) => false,
+ }
+}
+
/// Run the gradle commands, assemble and test
/// This will check to see if there is a gradlew file in the root of the directory
pub fn run(
@@ -134,12 +178,12 @@ fn build_gradle_command(config_path: &PathBuf, command: GradleCommand) -> Result
#[cfg(test)]
mod test {
use super::*;
-
+ const KOTLIN_CODE_DIR: &str = "./tests/kotlin-test-projects";
#[test]
#[should_panic(expected = "gradlew does not exist at the root of this project")]
fn test() {
run(
- &PathBuf::from("./kotlin-test-projects/no-gradle-project"),
+ &PathBuf::from(KOTLIN_CODE_DIR).join("no-gradle-project"),
&PathBuf::new(),
&PathBuf::new(),
&mut Mutation::new(
@@ -148,7 +192,7 @@ mod test {
"new_op".into(),
"old_op".into(),
0,
- crate::mutation_tool::MutationOperators::ArthimeticOperator,
+ crate::mutation_tool::MutationOperators::ArithmeticReplacementOperator,
"file_name".into(),
),
)
@@ -157,11 +201,13 @@ mod test {
#[test]
fn run_mutations_should_all_pass() {
- let dir = PathBuf::from("./kotlin-test-projects/mutations")
+ let dir = PathBuf::from(KOTLIN_CODE_DIR)
+ .join("mutations")
.read_dir()
.unwrap();
- let file_backup =
- include_str!("../kotlin-test-projects/kotlin-project/src/main/kotlin/Calculator.kt");
+ let file_backup = include_str!(
+ "../tests/kotlin-test-projects/kotlin-project/src/main/kotlin/Calculator.kt"
+ );
for entry in dir {
let entry = entry.unwrap().path();
let mut mutation = Mutation::new(
@@ -170,14 +216,14 @@ mod test {
"new_op".into(),
"old_op".into(),
0,
- crate::mutation_tool::MutationOperators::ArthimeticOperator,
+ crate::mutation_tool::MutationOperators::ArithmeticReplacementOperator,
"file_name".into(),
);
run(
- &PathBuf::from("./kotlin-test-projects/kotlin-project"),
+ &PathBuf::from(KOTLIN_CODE_DIR).join("kotlin-project"),
&entry,
&PathBuf::from(
- "./kotlin-test-projects/kotlin-project/src/main/kotlin/Calculator.kt",
+ "./tests/kotlin-test-projects/kotlin-project/src/main/kotlin/Calculator.kt",
),
&mut mutation,
)
@@ -185,7 +231,7 @@ mod test {
// Reset File
fs::write(
&PathBuf::from(
- "./kotlin-test-projects/kotlin-project/src/main/kotlin/Calculator.kt",
+ "./tests/kotlin-test-projects/kotlin-project/src/main/kotlin/Calculator.kt",
),
file_backup,
)
diff --git a/src/html_gen.rs b/src/html_gen.rs
index 8b03a6e..f5b9c7e 100644
--- a/src/html_gen.rs
+++ b/src/html_gen.rs
@@ -4,7 +4,7 @@ use horrorshow::{helper::doctype, html};
use crate::mutation_tool::{Mutation, MutationResult};
-pub fn build_html_page(data: &Vec) {
+pub fn build_html_page(data: &Vec, path: &Path) {
// Group the mutations by file name
let mut file_mutations = HashMap::new();
for mutation in data {
@@ -95,7 +95,153 @@ pub fn build_html_page(data: &Vec) {
}
);
// write file to kode-kraken-dist/report.html
- let file_path = Path::new("kode-kraken-dist").join("report.html");
- let mut file = File::create(file_path).unwrap();
+ let file_path = path.join("report.html");
+ let mut file = File::create(file_path).expect("Could not create report.html file");
file.write_all(report.as_bytes()).unwrap();
}
+
+#[cfg(test)]
+mod test {
+ use std::collections::HashMap;
+ use std::fs::File;
+ use std::io::Read;
+ use std::path::Path;
+
+ use crate::mutation_tool::{Mutation, MutationOperators, MutationResult};
+
+ use super::build_html_page;
+
+ #[test]
+ fn test_build_html_page() {
+ // Create some sample mutations
+ let mutation1 = Mutation::new(
+ 0,
+ 10,
+ "new_op1".to_string(),
+ "old_op1".to_string(),
+ 1,
+ MutationOperators::ArithmeticReplacementOperator,
+ "file1".to_string(),
+ );
+ let mutation2 = Mutation::new(
+ 0,
+ 15,
+ "new_op2".to_string(),
+ "old_op2".to_string(),
+ 2,
+ MutationOperators::AssignmentReplacementOperator,
+ "file1".to_string(),
+ );
+ let mutation3 = Mutation::new(
+ 0,
+ 8,
+ "new_op3".to_string(),
+ "old_op3".to_string(),
+ 1,
+ MutationOperators::ElvisLiteralChangeOperator,
+ "file2".to_string(),
+ );
+ let mutation4 = Mutation::new(
+ 0,
+ 12,
+ "new_op4".to_string(),
+ "old_op4".to_string(),
+ 3,
+ MutationOperators::LogicalReplacementOperator,
+ "file2".to_string(),
+ );
+
+ let mutations = vec![
+ mutation1.clone(),
+ mutation2.clone(),
+ mutation3.clone(),
+ mutation4.clone(),
+ ];
+
+ // Create kode-kraken-dist directory
+ std::fs::create_dir_all("kode-kraken-dist").unwrap();
+
+ // Create test data
+ let mut file_mutations = HashMap::new();
+ for mutation in &mutations {
+ let file_name = mutation.file_name.clone();
+ let file_mutations = file_mutations.entry(file_name).or_insert(Vec::new());
+ file_mutations.push(mutation.clone());
+ }
+
+ // Call the function
+ build_html_page(&mutations, Path::new("./kode-kraken-dist"));
+
+ // Read the generated HTML file
+ let file_path = Path::new("kode-kraken-dist").join("report.html");
+ let mut file_content = String::new();
+ File::open(file_path)
+ .expect("Failed to open the generated HTML file")
+ .read_to_string(&mut file_content)
+ .expect("Failed to read the generated HTML file");
+
+ // Verify that HTML content contains information about each file and mutation
+ assert_contains(
+ &file_content,
+ "Kode Kraken Results | ",
+ );
+ assert_contains(&file_content, "File Name | ");
+ assert_contains(&file_content, "# of Mutations | ");
+ assert_contains(&file_content, "# Survived | ");
+ assert_contains(&file_content, "# Killed | ");
+ assert_contains(&file_content, "Score | ");
+
+ for (file_name, fm) in file_mutations.iter() {
+ assert_contains(
+ &file_content,
+ &format!("{} | ", file_name),
+ );
+ assert_contains(
+ &file_content,
+ &format!("{} | ", fm.len()),
+ );
+ assert_contains(
+ &file_content,
+ &format!(
+ "{} | ",
+ fm.iter()
+ .filter(|m| m.result == MutationResult::Survived)
+ .count()
+ ),
+ );
+ assert_contains(
+ &file_content,
+ &format!(
+ "{} | ",
+ fm.iter()
+ .filter(|m| m.result == MutationResult::Killed)
+ .count()
+ ),
+ );
+ let score = (fm
+ .iter()
+ .filter(|m| m.result == MutationResult::Killed)
+ .count() as f32
+ / (fm.len()
+ - fm.iter()
+ .filter(|m| {
+ m.result != MutationResult::Killed
+ && m.result != MutationResult::Survived
+ })
+ .count()) as f32)
+ * 100.0;
+ assert_contains(
+ &file_content,
+ &format!("{:.2}% | ", score),
+ );
+ }
+ }
+
+ fn assert_contains(haystack: &str, needle: &str) {
+ assert!(
+ haystack.contains(needle),
+ "{}",
+ format!("Expected content:\n{}\nTo contain:\n{}", haystack, needle)
+ );
+ }
+}
diff --git a/src/lib.rs b/src/lib.rs
index 53e867a..a6ce7d4 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -5,5 +5,3 @@ pub mod gradle;
pub mod html_gen;
pub mod kotlin_types;
pub mod mutation_tool;
-#[cfg(test)]
-mod test_config;
diff --git a/src/main.rs b/src/main.rs
index 34528d6..ff6241b 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,35 +1,5 @@
-use kode_kraken::{cli::run_cli, config::KodeKrakenConfig, mutation_tool::OUT_DIRECTORY};
-use std::path::Path;
-use tracing_appender::non_blocking::WorkerGuard;
+use kode_kraken::cli::run_cli;
fn main() {
- let config = KodeKrakenConfig::load_config();
-
- let _guard = setup_logging(&config.logging.log_level);
- tracing::info!("Starting Kode Kraken");
- run_cli(config);
-}
-
-fn setup_logging(log_level: &str) -> WorkerGuard {
- let log_level = match log_level.to_lowercase().as_str() {
- "trace" => tracing::Level::TRACE,
- "debug" => tracing::Level::DEBUG,
- "info" => tracing::Level::INFO,
- "warn" => tracing::Level::WARN,
- "error" => tracing::Level::ERROR,
- _ => tracing::Level::INFO,
- };
- // Create dist log folder if it doesn't exist
- let log_dir = Path::new(OUT_DIRECTORY).join("logs");
- std::fs::create_dir_all(&log_dir).expect("Could not create log directory");
- let file_appender = tracing_appender::rolling::never(log_dir, "kode-kraken.log");
- let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
- tracing_subscriber::fmt()
- .with_max_level(log_level)
- .with_ansi(false)
- .with_target(false)
- .with_writer(non_blocking)
- .with_thread_ids(true)
- .init();
- guard
+ run_cli();
}
diff --git a/src/mutation_tool/builder.rs b/src/mutation_tool/builder.rs
index cd67053..3c7416c 100644
--- a/src/mutation_tool/builder.rs
+++ b/src/mutation_tool/builder.rs
@@ -1,11 +1,12 @@
+use std::path::Path;
+
use crate::{cli::MutationCommandConfig, config::KodeKrakenConfig};
-use super::{AllMutationOperators, MutationOperators, MutationTool, OUT_DIRECTORY};
+use super::MutationTool;
pub struct MutationToolBuilder {
mutate_config: Option,
kodekraken_config: Option,
- mutation_operators: Option>,
enable_mutation_comment: bool,
}
@@ -21,7 +22,6 @@ impl MutationToolBuilder {
Self {
mutate_config: None,
kodekraken_config: None,
- mutation_operators: None,
enable_mutation_comment: false,
}
}
@@ -38,12 +38,6 @@ impl MutationToolBuilder {
self
}
- /// Set the mutation operators to be used
- pub fn set_mutation_operators(mut self, mutation_operators: Vec) -> Self {
- self.mutation_operators = Some(mutation_operators);
- self
- }
-
/// Sets whether to enable the mutation comment
pub fn set_mutation_comment(mut self, enable_mutation_comment: bool) -> Self {
self.enable_mutation_comment = enable_mutation_comment;
@@ -53,16 +47,177 @@ impl MutationToolBuilder {
pub fn build(self) -> MutationTool {
let mutate_config = self.mutate_config.unwrap_or_default();
let kodekraken_config = self.kodekraken_config.unwrap_or_default();
- let mutation_operators = self
- .mutation_operators
- .unwrap_or(AllMutationOperators::new().get_mutation_operators());
+
+ let output_directory = Path::new(&mutate_config.path)
+ .join("kode-kraken-dist")
+ .to_str()
+ .unwrap_or_default()
+ .to_string();
MutationTool::new(
- mutate_config,
- kodekraken_config,
- OUT_DIRECTORY.into(),
- mutation_operators,
+ mutate_config.clone(),
+ kodekraken_config.clone(),
+ output_directory,
+ kodekraken_config.general.operators.clone(),
self.enable_mutation_comment,
)
.unwrap()
}
}
+#[cfg(test)]
+mod tests {
+ use crate::{config::GeneralConfig, mutation_tool::MutationOperators};
+ use std::env::temp_dir;
+
+ use super::*;
+ use std::sync::Arc;
+ use MutationOperators::*;
+
+ #[test]
+ fn test_default_builder() {
+ // Create temp directory
+ let temp_dir = temp_dir().join("default_builder");
+ // Create the temp directory
+ std::fs::create_dir_all(&temp_dir).unwrap();
+
+ let builder = MutationToolBuilder::new().set_mutate_config(MutationCommandConfig {
+ path: temp_dir.to_str().unwrap().to_string(),
+ });
+ let mutation_tool = builder.build();
+
+ // Add assertions based on your specific default values
+ assert_eq!(mutation_tool.enable_mutation_comment, false);
+ assert_eq!(mutation_tool.kodekraken_config, KodeKrakenConfig::new());
+ assert_eq!(
+ mutation_tool.mutate_config,
+ MutationCommandConfig {
+ path: temp_dir.to_str().unwrap().to_string(),
+ }
+ );
+ assert_eq!(
+ Arc::into_inner(mutation_tool.mutation_operators).unwrap(),
+ vec![
+ ArithmeticReplacementOperator,
+ UnaryRemovalOperator,
+ LogicalReplacementOperator,
+ RelationalReplacementOperator,
+ AssignmentReplacementOperator,
+ UnaryReplacementOperator,
+ NotNullAssertionOperator,
+ ElvisRemoveOperator,
+ ElvisLiteralChangeOperator,
+ LiteralChangeOpeator,
+ ]
+ );
+ }
+
+ #[test]
+ fn test_set_general_config() {
+ // Create temp directory
+ let temp_dir = temp_dir().join("set_general_config");
+ // Create the temp directory
+ std::fs::create_dir_all(&temp_dir).unwrap();
+
+ let general_config = KodeKrakenConfig {
+ general: GeneralConfig {
+ timeout: Some(10),
+ operators: vec![
+ MutationOperators::AssignmentReplacementOperator,
+ MutationOperators::UnaryRemovalOperator,
+ ],
+ },
+ ..Default::default()
+ };
+
+ let builder = MutationToolBuilder::new()
+ .set_mutate_config(MutationCommandConfig {
+ path: temp_dir.to_str().unwrap().to_string(),
+ })
+ .set_general_config(general_config.clone());
+ let mutation_tool = builder.build();
+
+ assert_eq!(mutation_tool.kodekraken_config.general.timeout, Some(10));
+ assert_eq!(
+ mutation_tool.kodekraken_config.general.operators,
+ vec![
+ MutationOperators::AssignmentReplacementOperator,
+ MutationOperators::UnaryRemovalOperator
+ ]
+ );
+ }
+
+ #[test]
+ fn test_set_mutate_config() {
+ // Create a temp directory
+ let temp_dir = temp_dir().join("set_mutate_config");
+ // Create the temp directory
+ std::fs::create_dir_all(&temp_dir).unwrap();
+
+ let mutate_config = MutationCommandConfig {
+ path: temp_dir.to_str().unwrap().to_string(),
+ };
+
+ let builder = MutationToolBuilder::new().set_mutate_config(mutate_config.clone());
+ let mutation_tool = builder.build();
+
+ // Add assertions based on your specific mutate_config fields
+ assert_eq!(mutation_tool.mutate_config, mutate_config);
+ assert_eq!(
+ mutation_tool.mutate_config.path,
+ temp_dir.to_str().unwrap().to_string()
+ );
+ }
+
+ #[test]
+ fn test_set_mutation_comment() {
+ // Create a temp directory
+ let temp_dir = temp_dir().join("set_mutation_comment");
+ // Create the temp directory
+ std::fs::create_dir_all(&temp_dir).unwrap();
+ let builder = MutationToolBuilder::new()
+ .set_mutate_config(MutationCommandConfig {
+ path: temp_dir.to_str().unwrap().to_string(),
+ })
+ .set_mutation_comment(true);
+ let mutation_tool = builder.build();
+
+ assert_eq!(mutation_tool.enable_mutation_comment, true);
+ }
+
+ #[test]
+ fn test_build_with_defaults() {
+ // Create a temp directory
+ let temp_dir = temp_dir().join("build_with_defaults");
+ // Create the temp directory
+ std::fs::create_dir_all(&temp_dir).unwrap();
+
+ let builder = MutationToolBuilder::new().set_mutate_config(MutationCommandConfig {
+ path: temp_dir.to_str().unwrap().to_string(),
+ });
+ let mutation_tool = builder.build();
+
+ // Add assertions based on your specific default values
+ assert_eq!(mutation_tool.enable_mutation_comment, false);
+ assert_eq!(mutation_tool.kodekraken_config, KodeKrakenConfig::default());
+ assert_eq!(
+ mutation_tool.mutate_config,
+ MutationCommandConfig {
+ path: temp_dir.to_str().unwrap().to_string(),
+ }
+ );
+ assert_eq!(
+ Arc::into_inner(mutation_tool.mutation_operators).unwrap(),
+ vec![
+ ArithmeticReplacementOperator,
+ UnaryRemovalOperator,
+ LogicalReplacementOperator,
+ RelationalReplacementOperator,
+ AssignmentReplacementOperator,
+ UnaryReplacementOperator,
+ NotNullAssertionOperator,
+ ElvisRemoveOperator,
+ ElvisLiteralChangeOperator,
+ LiteralChangeOpeator,
+ ]
+ );
+ }
+}
diff --git a/src/mutation_tool/mod.rs b/src/mutation_tool/mod.rs
index 63fc5b2..2f0e80d 100644
--- a/src/mutation_tool/mod.rs
+++ b/src/mutation_tool/mod.rs
@@ -7,3 +7,129 @@ pub use builder::*;
pub use mutation::*;
pub use operators::*;
pub use tool::*;
+
+pub fn debug_print_ast(ast: &tree_sitter::Node, spaces: usize) {
+ // Print the node
+ println!("{}{:?}", " ".repeat(spaces), ast.kind());
+ // Go through the children
+ for child in ast.children(&mut ast.walk()) {
+ debug_print_ast(&child, spaces + 2);
+ }
+}
+
+#[cfg(test)]
+pub mod test_util {
+ pub const KOTLIN_TEST_CODE: &str = r#"
+fun main() {
+ // Arithmetic expressions
+ val a = 10
+ val b = 3
+ val c = a + b
+ val d = a - b
+ val e = a * b
+ val f = a / b
+ val g = a % b
+}
+"#;
+
+ pub const KOTLIN_RELATIONAL_TEST_CODE: &str = r#"
+fun main() {
+ // Relational expressions
+ val a = 10
+ val b = 3
+ val c = a > b
+ val d = a < b
+ val e = a >= b
+ val f = a <= b
+ val g = a == b
+ val h = a != b
+}
+"#;
+
+ pub const KOTLIN_LOGICAL_TEST_CODE: &str = r#"
+fun main() {
+ // Logical expressions
+ val a = true
+ val b = false
+ val c = a && b
+ val d = a || b
+}
+"#;
+
+ pub const KOTLIN_UNARY_TEST_CODE: &str = r#"
+var h = 5
+h++
+h--
+++h
+--h
+"#;
+
+ pub const KOTLIN_UNARY_REMOVAL_TEST_CODE: &str = r#"
+var h = 5
+h++
+h--
+++h
+--h
+val a = !h
+val b = -h
+val c = +h
+"#;
+
+ pub const KOTLIN_ASSIGNMENT_TEST_CODE: &str = r#"
+var h = 5
+h += 3
+h -= 1
+h *= 2
+h /= 4
+h %= 2
+"#;
+
+ pub const KOTLIN_ELVIS_TEST_CODE: &str = r#"
+fun main() {
+ val a = 10
+ val b = 3
+ val c = a ?: b
+}
+"#;
+
+ pub const KOTLIN_ELVIS_LITERAL_TEST_CODE: &str = r#"
+fun main() {
+ val a: String? = null
+ val b = a ?: "b"
+ val c: Int? = null
+ val d = c ?: 1
+ val e = c ?: -10
+ val f: Boolean? = null
+ val g = e ?: true
+ val h: Double? = null
+ val i = h ?: 2.0
+ val j = h ?: -3.0
+ val k: Float? = null
+ val l = k ?: 4.0f
+ val m = k ?: -5.0f
+ val n: Long? = null
+ val o = n ?: 6L
+ val p = n ?: -7L
+ val q: Char? = null
+ val r = q ?: 'a'
+ val s = q ?: 'b'
+}
+"#;
+
+ pub const KOTLIN_LITERAL_TEST_CODE: &str = r#"
+fun main() {
+ val a = 1
+ val b = -2
+ val c = 4.0
+ val d = -6.0
+ val e = 12L
+ val f = -13L
+ val g = 14.0f
+ val h = -16.0f
+ val i = true
+ val j = false
+ val k = 'a'
+ val l = "a"
+}
+"#;
+}
diff --git a/src/mutation_tool/mutation.rs b/src/mutation_tool/mutation.rs
index 24ae937..336121e 100644
--- a/src/mutation_tool/mutation.rs
+++ b/src/mutation_tool/mutation.rs
@@ -35,26 +35,36 @@ impl Default for MutationResult {
}
#[derive(Debug, Clone, Table, serde::Serialize)]
+/// Represents a mutation applied to a code file.
pub struct Mutation {
+ /// The unique identifier for the mutation.
#[table(title = "Id")]
#[serde(skip)]
pub id: Uuid,
+ /// The starting byte of the old operator.
#[table(skip)]
#[serde(skip)]
pub start_byte: usize,
+ /// The ending byte of the old operator.
#[table(skip)]
#[serde(skip)]
pub end_byte: usize,
+ /// The name of the file that was mutated.
#[table(title = "File Name")]
pub file_name: String,
+ /// The line number where the mutation was applied.
#[table(title = "Line Number")]
pub line_number: usize,
+ /// The new operator that was applied.
#[table(title = "New Operator")]
pub new_op: String,
+ /// The old operator that was replaced.
#[table(title = "Old Operator")]
pub old_op: String,
+ /// The type of mutation that was applied.
#[table(title = "Mutation Type")]
pub mutation_type: MutationOperators,
+ /// The result of the mutation.
#[table(title = "Result")]
pub result: MutationResult,
}
@@ -109,3 +119,104 @@ impl Display for Mutation {
pub struct FileMutations {
pub mutations: Vec,
}
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_default_mutation_result() {
+ let default_result = MutationResult::default();
+ assert_eq!(default_result, MutationResult::InProgress);
+ }
+
+ #[test]
+ fn test_display_mutation_result() {
+ let result = MutationResult::Timeout;
+ let formatted_result = format!("{}", result);
+ assert_eq!(formatted_result, "Timeout");
+ }
+
+ #[test]
+ fn test_create_mutation() {
+ let mutation = Mutation::new(
+ 10, // start_byte
+ 20, // end_byte
+ "new_op".to_string(),
+ "old_op".to_string(),
+ 42, // line_number
+ MutationOperators::ArithmeticReplacementOperator,
+ "example.rs".to_string(),
+ );
+
+ assert_eq!(mutation.start_byte, 10);
+ assert_eq!(mutation.end_byte, 20);
+ assert_eq!(mutation.new_op, "new_op");
+ assert_eq!(mutation.old_op, "old_op");
+ assert_eq!(mutation.line_number, 42);
+ assert_eq!(
+ mutation.mutation_type,
+ MutationOperators::ArithmeticReplacementOperator
+ );
+ assert_eq!(mutation.file_name, "example.rs");
+ assert_ne!(mutation.id, Uuid::nil()); // Ensure that UUID is not nil
+ assert_eq!(mutation.result, MutationResult::InProgress);
+ }
+
+ #[test]
+ fn test_display_mutation() {
+ let mutation = Mutation::new(
+ 10, // start_byte
+ 20, // end_byte
+ "new_op".to_string(),
+ "old_op".to_string(),
+ 42, // line_number
+ MutationOperators::ArithmeticReplacementOperator,
+ "example.rs".to_string(),
+ );
+
+ let expected_output = format!(
+ "
+ /**
+ AUTO GENERATED COMMENT
+ Mutation Operator: ArithmeticReplacementOperator
+ Line number: {}
+ Id: {},
+ Old Operator: old_op,
+ New Operator: new_op
+ */",
+ (42 + 9),
+ mutation.id
+ );
+
+ assert_eq!(format!("{}", mutation), expected_output);
+ }
+
+ #[test]
+ fn test_create_file_mutations() {
+ let mutations = vec![
+ Mutation::new(
+ 10, // start_byte
+ 20, // end_byte
+ "new_op1".to_string(),
+ "old_op1".to_string(),
+ 42, // line_number
+ MutationOperators::ArithmeticReplacementOperator,
+ "example.rs".to_string(),
+ ),
+ Mutation::new(
+ 30, // start_byte
+ 40, // end_byte
+ "new_op2".to_string(),
+ "old_op2".to_string(),
+ 56, // line_number
+ MutationOperators::UnaryRemovalOperator,
+ "example.rs".to_string(),
+ ),
+ ];
+
+ let file_mutations = FileMutations { mutations };
+
+ assert_eq!(file_mutations.mutations.len(), 2);
+ // Add more assertions based on your specific requirements
+ }
+}
diff --git a/src/mutation_tool/operators.rs b/src/mutation_tool/operators.rs
index 9efb20f..9b4778b 100644
--- a/src/mutation_tool/operators.rs
+++ b/src/mutation_tool/operators.rs
@@ -1,26 +1,25 @@
+use rand::{distributions::uniform::SampleUniform, Rng};
+
use crate::{
error::{KodeKrakenError, Result},
kotlin_types::KotlinTypes,
mutation_tool::Mutation,
};
-use std::{collections::HashSet, fmt::Display};
-
-// Struct that stores all the mutations operators by default
-#[derive(Clone)]
-pub struct AllMutationOperators {
- mutation_operators: Vec,
-}
+use std::{collections::HashSet, fmt::Display, fs};
// The different types of mutation operators that can be performed on a file
-#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize)]
+#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
pub enum MutationOperators {
- ArthimeticOperator,
+ ArithmeticReplacementOperator,
UnaryRemovalOperator,
- LogicalOperator,
- RelationalOperator,
- AssignmentOperator,
- UnaryOperator,
+ LogicalReplacementOperator,
+ RelationalReplacementOperator,
+ AssignmentReplacementOperator,
+ UnaryReplacementOperator,
NotNullAssertionOperator,
+ ElvisRemoveOperator,
+ ElvisLiteralChangeOperator,
+ LiteralChangeOpeator,
}
impl Display for MutationOperators {
@@ -29,13 +28,16 @@ impl Display for MutationOperators {
f,
"{}",
match self {
- MutationOperators::ArthimeticOperator => "ArthimeticOperator",
+ MutationOperators::ArithmeticReplacementOperator => "ArithmeticReplacementOperator",
MutationOperators::UnaryRemovalOperator => "UnaryRemovalOperator",
- MutationOperators::LogicalOperator => "LogicalOperator",
- MutationOperators::RelationalOperator => "RelationalOperator",
- MutationOperators::AssignmentOperator => "AssignmentOperator",
- MutationOperators::UnaryOperator => "UnaryOperator",
+ MutationOperators::LogicalReplacementOperator => "LogicalReplacementOperator",
+ MutationOperators::RelationalReplacementOperator => "RelationalReplacementOperator",
+ MutationOperators::AssignmentReplacementOperator => "AssignmentReplacementOperator",
+ MutationOperators::UnaryReplacementOperator => "UnaryReplacementOperator",
MutationOperators::NotNullAssertionOperator => "NotNullAssertionOperator",
+ MutationOperators::ElvisRemoveOperator => "ElvisRemoveOperator",
+ MutationOperators::ElvisLiteralChangeOperator => "ElvisLiteralChangeOperator",
+ MutationOperators::LiteralChangeOpeator => "LiteralChangeOpeator",
}
)
}
@@ -45,7 +47,7 @@ impl MutationOperators {
/// Get the operators that correspond to the mutation operator
fn get_operators(&self) -> HashSet {
match self {
- MutationOperators::ArthimeticOperator => vec![
+ MutationOperators::ArithmeticReplacementOperator => vec![
KotlinTypes::NonNamedType("+".to_string()),
KotlinTypes::NonNamedType("-".to_string()),
KotlinTypes::NonNamedType("*".to_string()),
@@ -61,13 +63,13 @@ impl MutationOperators {
]
.into_iter()
.collect(),
- MutationOperators::LogicalOperator => vec![
+ MutationOperators::LogicalReplacementOperator => vec![
KotlinTypes::NonNamedType("&&".to_string()),
KotlinTypes::NonNamedType("||".to_string()),
]
.into_iter()
.collect(),
- MutationOperators::RelationalOperator => vec![
+ MutationOperators::RelationalReplacementOperator => vec![
KotlinTypes::NonNamedType("==".to_string()),
KotlinTypes::NonNamedType("!=".to_string()),
KotlinTypes::NonNamedType("<".to_string()),
@@ -77,7 +79,7 @@ impl MutationOperators {
]
.into_iter()
.collect(),
- MutationOperators::AssignmentOperator => vec![
+ MutationOperators::AssignmentReplacementOperator => vec![
KotlinTypes::NonNamedType("=".to_string()),
KotlinTypes::NonNamedType("+=".to_string()),
KotlinTypes::NonNamedType("-=".to_string()),
@@ -87,7 +89,7 @@ impl MutationOperators {
]
.into_iter()
.collect(),
- MutationOperators::UnaryOperator => vec![
+ MutationOperators::UnaryReplacementOperator => vec![
KotlinTypes::NonNamedType("!".to_string()),
KotlinTypes::NonNamedType("++".to_string()),
KotlinTypes::NonNamedType("--".to_string()),
@@ -102,31 +104,69 @@ impl MutationOperators {
]
.into_iter()
.collect(),
+ MutationOperators::ElvisRemoveOperator
+ | MutationOperators::ElvisLiteralChangeOperator => {
+ vec![KotlinTypes::NonNamedType("?:".to_string())]
+ .into_iter()
+ .collect()
+ }
+ MutationOperators::LiteralChangeOpeator => vec![
+ KotlinTypes::IntegerLiteral,
+ KotlinTypes::LineStringLiteral,
+ KotlinTypes::BooleanLiteral,
+ KotlinTypes::LongLiteral,
+ KotlinTypes::RealLiteral,
+ KotlinTypes::CharacterLiteral,
+ ]
+ .into_iter()
+ .collect(),
}
}
/// Get the parent types that are necessary for the mutation operator
fn get_parent_necessary_types(&self) -> Vec {
match self {
- MutationOperators::ArthimeticOperator => vec![
+ MutationOperators::ArithmeticReplacementOperator => vec![
KotlinTypes::AdditiveExpression,
KotlinTypes::MultiplicativeExpression,
],
MutationOperators::UnaryRemovalOperator => vec![KotlinTypes::PrefixExpression],
- MutationOperators::LogicalOperator => vec![
+ MutationOperators::LogicalReplacementOperator => vec![
KotlinTypes::ConjunctionExpression,
KotlinTypes::DisjunctionExpression,
],
- MutationOperators::RelationalOperator => vec![
+ MutationOperators::RelationalReplacementOperator => vec![
KotlinTypes::EqualityExpression,
KotlinTypes::ComparisonExpression,
],
- MutationOperators::AssignmentOperator => vec![KotlinTypes::Assignment],
- MutationOperators::UnaryOperator => vec![
+ MutationOperators::AssignmentReplacementOperator => vec![KotlinTypes::Assignment],
+ MutationOperators::UnaryReplacementOperator => vec![
KotlinTypes::PostfixExpression,
KotlinTypes::PrefixExpression,
],
MutationOperators::NotNullAssertionOperator => vec![KotlinTypes::PostfixExpression],
+ MutationOperators::ElvisRemoveOperator
+ | MutationOperators::ElvisLiteralChangeOperator => {
+ vec![KotlinTypes::ElvisExpression]
+ }
+ MutationOperators::LiteralChangeOpeator => vec![
+ KotlinTypes::IntegerLiteral,
+ KotlinTypes::LineStringLiteral,
+ KotlinTypes::BooleanLiteral,
+ KotlinTypes::LongLiteral,
+ KotlinTypes::RealLiteral,
+ KotlinTypes::CharacterLiteral,
+ KotlinTypes::PrefixExpression,
+ KotlinTypes::PostfixExpression,
+ KotlinTypes::AdditiveExpression,
+ KotlinTypes::MultiplicativeExpression,
+ KotlinTypes::ConjunctionExpression,
+ KotlinTypes::DisjunctionExpression,
+ KotlinTypes::EqualityExpression,
+ KotlinTypes::ComparisonExpression,
+ KotlinTypes::PropertyDeclaration,
+ KotlinTypes::VariableDeclaration,
+ ],
}
}
@@ -138,8 +178,16 @@ impl MutationOperators {
self.mutate(root, &mut cursor, None, &mut mutations, file_name);
mutations
}
-
- /// Recursive function that finds all the mutations that can be made to the file
+ /// Mutates the given `root` node and its children using the provided `cursor`, `parent`, `mutations_made`, `file_name`, `operators`, and `parent_necessary_types`.
+ ///
+ /// # Arguments
+ ///
+ /// * `root` - A `tree_sitter::Node` representing the root node to mutate.
+ /// * `cursor` - A mutable reference to a `tree_sitter::TreeCursor` used to traverse the syntax tree.
+ /// * `parent` - An optional `tree_sitter::Node` representing the parent node of `root`.
+ /// * `mutations_made` - A mutable reference to a `Vec` that will be populated with any mutations made during the function's execution.
+ /// * `file_name` - A `String` representing the name of the file being mutated.
+ ///
fn mutate(
&self,
root: tree_sitter::Node,
@@ -154,31 +202,37 @@ impl MutationOperators {
.map(|p| KotlinTypes::new(p.kind()).expect("Failed to convert to KotlinType"));
mutations_made.append(
&mut self
- .mutate_operator(
- &node,
- &root_type,
- &parent_type,
- self.get_operators(),
- self.get_parent_necessary_types(),
- file_name,
- )
+ .mutate_operator(&node, &root_type, &parent_type, file_name)
.expect("Failed to mutate an operator"),
);
self.mutate(node, cursor, Some(node), mutations_made, file_name);
});
}
- /// Checks to see if the mutation operator can be applied to the node
+ /// Mutates the given root node based on the specified mutation operators and returns a vector of mutations made.
+ ///
+ /// # Arguments
+ ///
+ /// * `root_node` - A reference to the root node of the AST.
+ /// * `root` - A reference to the root type of the AST.
+ /// * `parent` - An optional reference to the parent type of the AST.
+ /// * `mutation_operators` - A HashSet of mutation operators to apply.
+ /// * `parent_types` - A vector of parent types to check against.
+ /// * `file_name` - The name of the file being mutated.
+ ///
+ /// # Returns
+ ///
+ /// A Result containing a vector of mutations made.
fn mutate_operator(
&self,
root_node: &tree_sitter::Node,
root: &KotlinTypes,
parent: &Option,
- mutation_operators: HashSet,
- parent_types: Vec,
file_name: &str,
) -> Result> {
let mut mutations_made = Vec::new();
+ let mutation_operators = self.get_operators();
+ let parent_types = self.get_parent_necessary_types();
// Check to see if root type is in the mutation_operators
// Check to see if the parent exists
// Checks to see if the parent is the necessary kotlin type
@@ -189,78 +243,232 @@ impl MutationOperators {
return Ok(mutations_made);
}
- if *self == MutationOperators::UnaryRemovalOperator {
- // If the operator is a unary removal operator, we just remove the operator
- let mutation = Mutation::new(
- root_node.start_byte(),
- root_node.end_byte(),
- KotlinTypes::RemoveOperator.to_string(),
- root.as_str(),
- root_node.start_position().row + 1,
- self.clone(),
- file_name.to_string(),
- );
- mutations_made.push(mutation);
- return Ok(mutations_made);
- }
-
- // Create a mutant for all mutation operators
- mutation_operators.iter().for_each(|operator| {
- if operator != root {
+ match *self {
+ MutationOperators::UnaryRemovalOperator => {
+ // If the operator is a unary removal operator, we just remove the operator
let mutation = Mutation::new(
root_node.start_byte(),
root_node.end_byte(),
- operator.to_string(),
+ KotlinTypes::RemoveOperator.to_string(),
root.as_str(),
root_node.start_position().row + 1,
self.clone(),
file_name.to_string(),
);
- mutations_made.push(mutation)
+ mutations_made.push(mutation);
}
- });
- Ok(mutations_made)
- }
-}
+ MutationOperators::ElvisRemoveOperator => {
+ // If the operator is a Remove elvis operator, we remove the operator and everything after it
+ // Get the end byte of the end of the line
+ let end_byte = root_node.parent().unwrap().end_byte(); // TODO: remove unwrap
-impl AllMutationOperators {
- pub fn new() -> Self {
- Self {
- mutation_operators: vec![
- MutationOperators::ArthimeticOperator,
- MutationOperators::UnaryRemovalOperator,
- MutationOperators::LogicalOperator,
- MutationOperators::RelationalOperator,
- MutationOperators::AssignmentOperator,
- MutationOperators::UnaryOperator,
- MutationOperators::NotNullAssertionOperator,
- ],
+ let mutation = Mutation::new(
+ root_node.start_byte(),
+ end_byte,
+ KotlinTypes::RemoveOperator.to_string(),
+ root.as_str(),
+ root_node.start_position().row + 1,
+ self.clone(),
+ file_name.to_string(),
+ );
+ mutations_made.push(mutation);
+ }
+ MutationOperators::ElvisLiteralChangeOperator
+ | MutationOperators::LiteralChangeOpeator => {
+ self.mutate_literal(&root_node.parent().unwrap(), &mut mutations_made, file_name)
+ }
+ _ => {
+ // Create a mutant for all mutation operators
+ mutation_operators.iter().for_each(|operator| {
+ if operator != root {
+ let mutation = Mutation::new(
+ root_node.start_byte(),
+ root_node.end_byte(),
+ operator.to_string(),
+ root.as_str(),
+ root_node.start_position().row + 1,
+ self.clone(),
+ file_name.to_string(),
+ );
+ mutations_made.push(mutation)
+ }
+ });
+ }
}
- }
- pub fn get_mutation_operators(&self) -> Vec {
- self.mutation_operators.clone()
+ Ok(mutations_made)
}
-}
-impl Default for AllMutationOperators {
- fn default() -> Self {
- Self::new()
+ fn mutate_literal(
+ &self,
+ root_node: &tree_sitter::Node,
+ mutations_made: &mut Vec,
+ file_name: &str,
+ ) {
+ let file = fs::read(file_name).expect("Failed to read file");
+ let file = file.as_slice();
+ let children = root_node
+ .children(&mut root_node.walk())
+ .collect::>();
+ let node = match children.iter().last() {
+ Some(node) => node,
+ None => root_node,
+ };
+
+ let child_type = KotlinTypes::new(node.kind()).expect("Failed to convert to KotlinType");
+ // Change the literal to a different literal
+ let mut val = node.utf8_text(file).unwrap();
+ match child_type {
+ KotlinTypes::IntegerLiteral => {
+ let val = val.parse::().unwrap();
+ // Change the value and create a mutation
+ let mutated_val = generate_random_literal(val, i32::MIN, i32::MAX);
+ let mutation = Mutation::new(
+ node.start_byte(),
+ node.end_byte(),
+ mutated_val.to_string(),
+ val.to_string(),
+ node.start_position().row + 1,
+ self.clone(),
+ file_name.to_string(),
+ );
+ mutations_made.push(mutation);
+ }
+ KotlinTypes::PrefixExpression => {
+ // In this case, we need to see the type of the prefix expression, so we need to
+ // Recurse down to the literal
+ self.mutate_literal(node, mutations_made, file_name)
+ }
+ KotlinTypes::LineStringLiteral => {
+ // Replace the string with a different string
+ let val = r#""Hello I am a Mutant!""#.to_string();
+
+ let mutation = Mutation::new(
+ node.start_byte(),
+ node.end_byte(),
+ val,
+ node.utf8_text(file).unwrap().to_string(),
+ node.start_position().row + 1,
+ self.clone(),
+ file_name.to_string(),
+ );
+ mutations_made.push(mutation);
+ }
+ KotlinTypes::BooleanLiteral => {
+ let val = val.parse::().unwrap();
+ // Change the value and create a mutation
+ let mutated_val = !val;
+
+ let mutation = Mutation::new(
+ node.start_byte(),
+ node.end_byte(),
+ mutated_val.to_string(),
+ val.to_string(),
+ node.start_position().row + 1,
+ self.clone(),
+ file_name.to_string(),
+ );
+ mutations_made.push(mutation);
+ }
+ KotlinTypes::LongLiteral => {
+ // Need to strip off the l at the end
+ if val.ends_with("L") {
+ val = val.strip_suffix("L").unwrap();
+ }
+
+ let val = val.parse::().unwrap();
+ // Change the value and create a mutation
+ let mutated_val = generate_random_literal(val, i64::MIN, i64::MAX);
+
+ let mutation = Mutation::new(
+ node.start_byte(),
+ node.end_byte(),
+ mutated_val.to_string(),
+ val.to_string(),
+ node.start_position().row + 1,
+ self.clone(),
+ file_name.to_string(),
+ );
+ mutations_made.push(mutation);
+ }
+ KotlinTypes::RealLiteral => {
+ // Need to strip off the f at the end
+ if val.ends_with("f") {
+ val = val.strip_suffix("f").unwrap();
+ }
+ let val = val.parse::().unwrap();
+ // Change the value and create a mutation
+ let mutated_val = generate_random_literal(val, 0.0, 1.0);
+ let mutation = Mutation::new(
+ node.start_byte(),
+ node.end_byte(),
+ mutated_val.to_string(),
+ val.to_string(),
+ node.start_position().row + 1,
+ self.clone(),
+ file_name.to_string(),
+ );
+
+ mutations_made.push(mutation);
+ }
+ KotlinTypes::CharacterLiteral => {
+ // Remove the single quotes and get the value
+ let val = val
+ .strip_prefix("'")
+ .unwrap()
+ .strip_suffix("'")
+ .unwrap()
+ .chars()
+ .next()
+ .unwrap();
+
+ // Get a random character between 'a' and 'z'
+ let mut rnd_val = rand::thread_rng().gen_range(b'a'..b'z') as char;
+ while rnd_val == val {
+ rnd_val = rand::thread_rng().gen_range(b'a'..b'z') as char;
+ }
+ let mut_val = format!("'{}'", rnd_val);
+ let mutation = Mutation::new(
+ node.start_byte(),
+ node.end_byte(),
+ mut_val,
+ val.to_string(),
+ node.start_position().row + 1,
+ self.clone(),
+ file_name.to_string(),
+ );
+ mutations_made.push(mutation);
+ }
+ _ => {}
+ }
}
}
-impl Iterator for AllMutationOperators {
- type Item = MutationOperators;
+fn generate_random_literal(original_literal: T, min: T, max: T) -> T
+where
+ T: std::cmp::PartialOrd + std::cmp::PartialEq + Copy + SampleUniform,
+{
+ let mut rng = rand::thread_rng();
+
+ // Generate a random integer different from the original literal
+ let mut random_literal = rng.gen_range(min..max);
- fn next(&mut self) -> Option {
- self.mutation_operators.pop()
+ // Ensure the random literal is different from the original
+ while random_literal == original_literal {
+ random_literal = rng.gen_range(min..max);
}
+
+ random_literal
}
#[cfg(test)]
mod tests {
+ use std::{env::temp_dir, io::Write};
+
+ use crate::mutation_tool::test_util::*;
+
use super::*;
- use crate::test_config::*;
+ use crate::mutation_tool::debug_print_ast;
use tree_sitter::Parser;
fn get_ast(text: &str) -> tree_sitter::Tree {
@@ -276,7 +484,7 @@ mod tests {
let tree = get_ast(KOTLIN_TEST_CODE);
let root = tree.root_node();
let mut mutations_made = Vec::new();
- MutationOperators::ArthimeticOperator.mutate(
+ MutationOperators::ArithmeticReplacementOperator.mutate(
root,
&mut root.walk(),
None,
@@ -295,7 +503,7 @@ mod tests {
let tree = get_ast(KOTLIN_RELATIONAL_TEST_CODE);
let root = tree.root_node();
let mut mutations_made = Vec::new();
- MutationOperators::RelationalOperator.mutate(
+ MutationOperators::RelationalReplacementOperator.mutate(
root,
&mut root.walk(),
None,
@@ -315,7 +523,7 @@ mod tests {
let tree = get_ast(KOTLIN_LOGICAL_TEST_CODE);
let root = tree.root_node();
let mut mutations_made = Vec::new();
- MutationOperators::LogicalOperator.mutate(
+ MutationOperators::LogicalReplacementOperator.mutate(
root,
&mut root.walk(),
None,
@@ -335,7 +543,7 @@ mod tests {
let tree = get_ast(KOTLIN_ASSIGNMENT_TEST_CODE);
let root = tree.root_node();
let mut mutations_made = Vec::new();
- MutationOperators::AssignmentOperator.mutate(
+ MutationOperators::AssignmentReplacementOperator.mutate(
root,
&mut root.walk(),
None,
@@ -355,7 +563,7 @@ mod tests {
let tree = get_ast(KOTLIN_UNARY_TEST_CODE);
let root = tree.root_node();
let mut mutations_made = Vec::new();
- MutationOperators::UnaryOperator.mutate(
+ MutationOperators::UnaryReplacementOperator.mutate(
root,
&mut root.walk(),
None,
@@ -391,12 +599,85 @@ mod tests {
}
}
+ #[test]
+ fn test_elvis_remove_operator() {
+ let tree = get_ast(KOTLIN_ELVIS_TEST_CODE);
+ let root = tree.root_node();
+ let mut mutations_made = Vec::new();
+ MutationOperators::ElvisRemoveOperator.mutate(
+ root,
+ &mut root.walk(),
+ None,
+ &mut mutations_made,
+ &"".into(),
+ );
+ assert_eq!(mutations_made.len(), 1);
+ // Assert that the old operator is not the same as the new operator
+ for mutation in mutations_made {
+ assert_ne!(mutation.old_op, mutation.new_op);
+ assert_eq!(mutation.new_op, "".to_string());
+ }
+ }
+
+ #[test]
+ fn test_elvis_literal_operator() {
+ // Create a temp file
+ let temp_dir = temp_dir();
+ let temp_file = temp_dir.join("temp_file.kt");
+ let mut file = fs::File::create(&temp_file).expect("Failed to create temp file");
+ file.write_all(KOTLIN_ELVIS_LITERAL_TEST_CODE.as_bytes())
+ .expect("Failed to write to temp file");
+
+ let tree = get_ast(KOTLIN_ELVIS_LITERAL_TEST_CODE);
+
+ let root = tree.root_node();
+ let mut mutations_made = Vec::new();
+ MutationOperators::ElvisLiteralChangeOperator.mutate(
+ root,
+ &mut root.walk(),
+ None,
+ &mut mutations_made,
+ &temp_file.to_str().unwrap().to_string(),
+ );
+ assert_eq!(mutations_made.len(), 12);
+ // Assert that the old operator is not the same as the new operator
+ for mutation in mutations_made {
+ assert_ne!(mutation.old_op, mutation.new_op);
+ }
+ }
+
+ #[test]
+ fn test_literal_change_operator() {
+ // Create a temp file
+ let temp_dir = temp_dir();
+ let temp_file = temp_dir.join("literal_change_temp_file.kt");
+ let mut file = fs::File::create(&temp_file).expect("Failed to create temp file");
+ file.write_all(KOTLIN_LITERAL_TEST_CODE.as_bytes())
+ .expect("Failed to write to temp file");
+ let tree = get_ast(KOTLIN_LITERAL_TEST_CODE);
+ let root = tree.root_node();
+ debug_print_ast(&root, 0);
+ let mut mutations_made = Vec::new();
+ MutationOperators::LiteralChangeOpeator.mutate(
+ root,
+ &mut root.walk(),
+ None,
+ &mut mutations_made,
+ &temp_file.to_str().unwrap().to_string(),
+ );
+ assert_eq!(mutations_made.len(), 12);
+ // Assert that the old operator is not the same as the new operator
+ for mutation in mutations_made {
+ assert_ne!(mutation.old_op, mutation.new_op);
+ }
+ }
+
#[test]
fn test_arthimetic_operator_does_not_create_mutations() {
let tree = get_ast(KOTLIN_UNARY_REMOVAL_TEST_CODE);
let root = tree.root_node();
let mut mutations_made = Vec::new();
- MutationOperators::ArthimeticOperator.mutate(
+ MutationOperators::ArithmeticReplacementOperator.mutate(
root,
&mut root.walk(),
None,
@@ -411,7 +692,7 @@ mod tests {
let tree = get_ast(KOTLIN_UNARY_REMOVAL_TEST_CODE);
let root = tree.root_node();
let mut mutations_made = Vec::new();
- MutationOperators::RelationalOperator.mutate(
+ MutationOperators::RelationalReplacementOperator.mutate(
root,
&mut root.walk(),
None,
@@ -426,7 +707,7 @@ mod tests {
let tree = get_ast(KOTLIN_UNARY_REMOVAL_TEST_CODE);
let root = tree.root_node();
let mut mutations_made = Vec::new();
- MutationOperators::LogicalOperator.mutate(
+ MutationOperators::LogicalReplacementOperator.mutate(
root,
&mut root.walk(),
None,
@@ -441,7 +722,38 @@ mod tests {
let tree = get_ast(KOTLIN_UNARY_REMOVAL_TEST_CODE);
let root = tree.root_node();
let mut mutations_made = Vec::new();
- MutationOperators::AssignmentOperator.mutate(
+ MutationOperators::AssignmentReplacementOperator.mutate(
+ root,
+ &mut root.walk(),
+ None,
+ &mut mutations_made,
+ &"".into(),
+ );
+ assert_eq!(mutations_made.len(), 0);
+ }
+
+ #[test]
+ fn test_unary_removal_operator_does_not_create_mutations() {
+ let tree = get_ast(KOTLIN_ASSIGNMENT_TEST_CODE);
+ let mutations_made =
+ MutationOperators::UnaryRemovalOperator.find_mutation(&tree, &"file_name".into());
+ assert_eq!(mutations_made.len(), 0);
+ }
+
+ #[test]
+ fn test_unary_replacement_operator_does_not_create_mutations() {
+ let tree = get_ast(KOTLIN_ASSIGNMENT_TEST_CODE);
+ let mutations_made =
+ MutationOperators::UnaryReplacementOperator.find_mutation(&tree, &"file_name".into());
+ assert_eq!(mutations_made.len(), 0);
+ }
+
+ #[test]
+ fn test_remove_elvis_operator_does_not_create_mutations() {
+ let tree = get_ast(KOTLIN_UNARY_REMOVAL_TEST_CODE);
+ let root = tree.root_node();
+ let mut mutations_made = Vec::new();
+ MutationOperators::ElvisRemoveOperator.mutate(
root,
&mut root.walk(),
None,
diff --git a/src/mutation_tool/tool.rs b/src/mutation_tool/tool.rs
index fa8307d..e7b2b74 100644
--- a/src/mutation_tool/tool.rs
+++ b/src/mutation_tool/tool.rs
@@ -18,38 +18,46 @@ use crate::config::KodeKrakenConfig;
use crate::error::{self, KodeKrakenError, Result};
use crate::mutation_tool::{
mutation::{FileMutations, Mutation, MutationResult},
- AllMutationOperators, MutationOperators,
+ MutationOperators,
};
use crate::{gradle, html_gen};
-pub const OUT_DIRECTORY: &str = "./kode-kraken-dist";
+use super::MutationToolBuilder;
+
const MAX_BUILD_THREADS: f32 = 5f32;
pub struct MutationTool {
parser: Arc>,
- mutate_config: MutationCommandConfig,
- mutation_operators: Arc>,
- mutation_dir: PathBuf,
- backup_dir: PathBuf,
- enable_mutation_comment: bool,
+ pub mutate_config: MutationCommandConfig,
+ pub mutation_operators: Arc>,
+ pub output_directory: String,
+ pub mutation_dir: PathBuf,
+ pub backup_dir: PathBuf,
+ pub enable_mutation_comment: bool,
thread_pool: rayon::ThreadPool,
pub kodekraken_config: KodeKrakenConfig,
}
impl Default for MutationTool {
fn default() -> Self {
- Self::new(
- MutationCommandConfig::default(),
- KodeKrakenConfig::default(),
- OUT_DIRECTORY.into(),
- AllMutationOperators::new().get_mutation_operators(),
- false,
- )
- .unwrap()
+ MutationToolBuilder::new().build()
}
}
impl MutationTool {
+ /// Creates a new instance of the mutation tool with the given configuration.
+ ///
+ /// # Arguments
+ ///
+ /// * `mutate_config` - The mutation command configuration.
+ /// * `kodekraken_config` - The KodeKraken configuration.
+ /// * `output_directory` - The output directory for the mutated files.
+ /// * `mutation_operators` - The mutation operators to use.
+ /// * `enable_mutation_comment` - Whether to enable mutation comments in the mutated files.
+ ///
+ /// # Returns
+ ///
+ /// A `Result` containing the new instance of the mutation tool, or an error if one occurred.
pub fn new(
mutate_config: MutationCommandConfig,
kodekraken_config: KodeKrakenConfig,
@@ -102,6 +110,7 @@ impl MutationTool {
mutate_config,
parser: Arc::new(Mutex::new(parser)),
mutation_operators: Arc::new(mutation_operators),
+ output_directory,
mutation_dir,
backup_dir,
enable_mutation_comment,
@@ -110,6 +119,16 @@ impl MutationTool {
})
}
+ /// Creates a mutated file name by appending the mutation ID to the original file name.
+ ///
+ /// # Arguments
+ ///
+ /// * `file_name` - A string slice that holds the name of the original file.
+ /// * `mutation` - A reference to a `Mutation` struct that holds the mutation ID.
+ ///
+ /// # Returns
+ ///
+ /// A `Result` that contains a string with the mutated file name if successful, or a `KodeKrakenError` if an error occurs.
fn create_mutated_file_name(&self, file_name: &str, mutation: &Mutation) -> Result {
Ok(format!(
"{}_{}",
@@ -126,6 +145,8 @@ impl MutationTool {
))
}
+ /// Mutates the project by gathering files, gathering mutations per file, generating mutations per file,
+ /// building and testing, reporting results, saving results in csv, and generating an HTML report.
pub fn mutate(&mut self) -> Result<()> {
tracing::info!("Mutation tool started...");
// Phase 1: Get files from project
@@ -150,14 +171,14 @@ impl MutationTool {
self.save_results(&mutations)?;
// Phase 7: Generate HTML Report
println!("[7/7] 📊 Generating HTML report...");
- html_gen::build_html_page(&mutations);
+ html_gen::build_html_page(&mutations, &Path::new(self.output_directory.as_str()));
Ok(())
}
/// Store All Mutations into a json filed name mutations.json within kode-kraken-dist/mutations directory
fn store_mutations(&self, file_mutations: &HashMap) -> Result<()> {
std::fs::write(
- Path::new(OUT_DIRECTORY)
+ Path::new(self.output_directory.as_str())
.join("mutations")
.join("mutations.json"),
serde_json::to_string_pretty(file_mutations)
@@ -168,9 +189,20 @@ impl MutationTool {
Ok(())
}
+ /// Saves the given mutations to a CSV file located in the `OUT_DIRECTORY`.
+ ///
+ /// # Arguments
+ ///
+ /// * `mutations` - A reference to a vector of `Mutation` structs to be saved.
+ ///
+ /// # Errors
+ ///
+ /// Returns a `KodeKrakenError` if there was an error creating the CSV writer, serializing a mutation,
+ /// flushing the CSV writer, or writing to the output file.
+ ///
fn save_results(&self, mutations: &Vec) -> Result<()> {
let mut writer = csv::WriterBuilder::new()
- .from_path(Path::new(OUT_DIRECTORY).join("output.csv"))
+ .from_path(Path::new(self.output_directory.as_str()).join("output.csv"))
.map_err(|e| {
KodeKrakenError::Error(format!("Error while creating csv writer: {}", e))
})?;
@@ -185,6 +217,9 @@ impl MutationTool {
Ok(())
}
+ /// This function retrieves all files from the project directory specified in the `mutate_config` field of the `Tool` struct.
+ /// It returns a `Result` containing a `Vec` of `String`s representing the file paths.
+ /// If no files are found in the directory, it returns an `Err` containing a `KodeKrakenError`.
fn get_files_from_project(&self) -> Result> {
let start = Instant::now();
let mut existing_files: Vec = vec![];
@@ -268,36 +303,35 @@ impl MutationTool {
Ok(())
}
+ /// Builds and tests mutated files in parallel using a thread pool.
+ ///
+ /// # Arguments
+ ///
+ /// * `file_mutations` - A hashmap containing the mutations for each file.
+ ///
+ /// # Returns
+ ///
+ /// A `Result` containing a vector of `Mutation` structs.
+ ///
fn build_and_test(
&mut self,
file_mutations: &HashMap,
) -> Result> {
+ self.gradle_checks()?;
+
+ // Get total number of mutations
let num_mutations = file_mutations
.iter()
.fold(0, |acc, (_, fm)| acc + fm.mutations.len());
- let progress_bar = Arc::new(ProgressBar::new(num_mutations as u64));
- progress_bar.set_style(
- ProgressStyle::default_bar()
- .template("[{elapsed_precise}] {bar:40.cyan/blue} {pos:>7}/{len:7} {msg} - Running tests...")
- .map_err(|e| KodeKrakenError::Error(e.to_string()))?
- .progress_chars("=> "),
- );
+ // Set up progress bar
+ let progress_bar = create_progress_bar(num_mutations)?;
+ // Make Copies of all files
self.copy_files(file_mutations)?;
// Merge all mutants into one vector
- let mut all_mutations: Vec = vec![];
- for (_, fm) in file_mutations.iter() {
- for mutation in fm.mutations.iter() {
- all_mutations.push(mutation.clone());
- }
- }
- // Partition the mutations into chunks
- let chunk_size = ((all_mutations.len() as f32) / MAX_BUILD_THREADS) as usize;
- let mut chunks: Vec> = all_mutations
- .chunks(chunk_size)
- .map(|c| c.to_vec())
- .collect();
+ let mut chunks = create_mutation_chucks(file_mutations);
+
// Set up threading
let path = Arc::new(self.mutate_config.path.clone());
let mutation_dir = Arc::new(self.mutation_dir.clone());
@@ -310,7 +344,8 @@ impl MutationTool {
for chunck in chunks.iter_mut() {
// Create unique temp directory
let uuid = uuid::Uuid::new_v4();
- let mut td = Path::new(OUT_DIRECTORY).join(format!("temp/{}", uuid));
+ let mut td =
+ Path::new(self.output_directory.as_str()).join(format!("temp/{}", uuid));
fs::create_dir_all(&td).expect("Failed to create temp directory");
// Create directory structure inside temp directory that matches the original project
let dir = PathBuf::from(&self.mutate_config.path);
@@ -374,13 +409,47 @@ impl MutationTool {
});
progress_bar.finish();
// Delete temp directory
- if let Err(err) = fs::remove_dir_all(Path::new(OUT_DIRECTORY).join("temp")) {
+ if let Err(err) = fs::remove_dir_all(Path::new(self.output_directory.as_str()).join("temp"))
+ {
println!("[ERROR] Failed to delete kode-kraken-dist/temp directory. Please view logs for more information.");
tracing::error!("Failed to delete kode-kraken-dist/temp directory: {}", err);
}
Ok(chunks.into_iter().flatten().collect())
}
+ fn gradle_checks(&mut self) -> Result<()> {
+ if !gradle::is_gradle_installed() {
+ return Err(KodeKrakenError::Error(
+ "Gradle is not installed. Please install Gradle and try again.".into(),
+ ));
+ }
+ let path = PathBuf::from(&self.mutate_config.path);
+ if !gradle::build_project_success(&path)? {
+ return Err(KodeKrakenError::Error(
+ "Project does not build successfully. Please fix the errors and try again.".into(),
+ ));
+ }
+
+ Ok(if !gradle::project_tests_pass(&path)? {
+ return Err(KodeKrakenError::Error(
+ "Project tests do not pass. Please fix the errors and try again.".into(),
+ ));
+ })
+ }
+
+ /// Recursively creates a temporary directory and copies all files from the given directory into it,
+ /// excluding the "kode-kraken-dist" folder. If a file is named "gradlew" or "gradlew.bat", it is copied
+ /// to the temporary directory with the same permissions. All other files are written to the temporary
+ /// directory with their original contents.
+ ///
+ /// # Arguments
+ ///
+ /// * `dir` - The directory to copy files from.
+ /// * `temp_dir` - The temporary directory to create and copy files into.
+ ///
+ /// # Returns
+ ///
+ /// Returns `Ok(())` if the operation is successful, or an error if any file operations fail.
fn create_temp_directory(&self, dir: PathBuf, temp_dir: &Path) -> Result<()> {
for entry in dir.read_dir()? {
let path = entry?.path();
@@ -411,6 +480,9 @@ impl MutationTool {
Ok(())
}
+ /// Generates mutations for each file in the given `file_mutations` HashMap.
+ /// Each mutation is applied to the corresponding file, and the resulting mutated file is written to
+ /// the output directory specified in the `MutationTool` configuration.
fn generate_mutations_per_file(
&self,
file_mutations: &HashMap,
@@ -418,9 +490,11 @@ impl MutationTool {
tracing::info!("Generating mutations per file");
let start = Instant::now();
self.thread_pool.scope(|s| {
+ // Iterate over each file and generate mutations
file_mutations.iter().for_each(|(file_name, fm)| {
let file_str = fs::read_to_string(file_name).expect("Failed to read file");
s.spawn(move |_| {
+ // Iterate over each mutation and apply it to the file
fm.mutations.iter().for_each(|m| {
let new_op_bytes = m.new_op.as_bytes();
let mut file = file_str.as_bytes().to_vec();
@@ -460,29 +534,52 @@ impl MutationTool {
Ok(())
}
+ /// Gathers mutations for each file in the given list of existing files.
+ ///
+ /// # Arguments
+ ///
+ /// * `existing_files` - A mutable slice of strings representing the paths to the existing files.
+ ///
+ /// # Returns
+ ///
+ /// A `Result` containing a `HashMap` of `String` keys and `FileMutations` values, or an error if the operation fails.
+ ///
fn gather_mutations_per_file(
&mut self,
existing_files: &mut [String],
) -> Result> {
+ // Record the start time for measuring performance
let start = Instant::now();
+
+ // Create shared mutable state for collecting mutations and mutation count
let file_mutations: Arc>> =
Arc::new(Mutex::new(HashMap::new()));
let mutation_count = Arc::new(Mutex::new(0));
+
+ // Use thread pool to parallelize mutation gathering for each file
self.thread_pool.scope(|s| {
for file in existing_files {
+ // Clone shared state for each thread
let mutation_count = mutation_count.clone();
let file_mutations = file_mutations.clone();
let parser = self.parser.clone();
let mutation_operators = self.mutation_operators.clone();
+
+ // Spawn a thread for each file
s.spawn(move |_| {
+ // Parse the file content using the parser
let ast = parser
.lock()
.expect("Failed to lock parser")
.parse(fs::read_to_string(&file).expect("File Not Found!"), None)
.expect("Parsing file failed");
+
+ // Iterate through mutation operators to find mutations
for mut_op in mutation_operators.iter() {
// Get a list of mutations that can be made
let mutations = mut_op.find_mutation(&ast, file);
+
+ // Update mutation count and file mutations
*mutation_count
.lock()
.expect("Failed to lock mutation_count var") += mutations.len();
@@ -498,37 +595,56 @@ impl MutationTool {
});
}
});
+
+ // Record the end time and log the time taken to gather mutations
let end = Instant::now();
tracing::info!(
"Time to gather mutations: {}",
end.duration_since(start).as_secs()
);
+
+ // Unwrap and log the total mutation count
let mutation_count = Arc::try_unwrap(mutation_count)
.map_err(|_| KodeKrakenError::Error("Failed to unwrap mutation_count".to_string()))?
.into_inner()
.map_err(|_| KodeKrakenError::Error("Failed to unwrap mutation_count".to_string()))?;
tracing::info!("Mutations made to all files");
tracing::info!("Total mutations made: {}", mutation_count);
+
+ // Return an error if no mutations were found, otherwise return the collected file mutations
+ if mutation_count == 0 {
+ return Err(KodeKrakenError::Error(
+ "No mutations were found in the project".into(),
+ ));
+ }
Ok(Arc::try_unwrap(file_mutations)
.map_err(|_| KodeKrakenError::Error("Failed to unwrap file_mutations".to_string()))?
.into_inner()
.map_err(|_| KodeKrakenError::Error("Failed to unwrap file_mutations".to_string()))?)
}
- /*
- Take in path to directory and get all files that end with .kt
- */
+ /// Gets all files from the given directory and adds them to the given vector.
+ /// This function is recursive, so it will also get files from subdirectories.
+ /// It will ignore files and directories that match the ignore patterns in the `KodeKrakenConfig`.
+ /// It will also ignore the `kode-kraken-dist` directory.
+ /// If the given path is not a directory, it will return an error.
fn get_files_from_directory(
&self,
path: String,
existing_files: &mut Vec,
) -> Result<()> {
+ // Open the directory at the given path
let directory = Path::new(path.as_str())
.read_dir()
.map_err(|_| KodeKrakenError::MutationGatheringError)?;
+
+ // Iterate over entries in the directory
for entry in directory {
+ // Handle potential errors when reading directory entries
let entry = entry.map_err(|_| KodeKrakenError::MutationGatheringError)?;
let path = entry.path();
+
+ // Skip processing if the entry corresponds to a specific directory
if path
.file_name()
.ok_or(KodeKrakenError::MutationGatheringError)?
@@ -538,6 +654,8 @@ impl MutationTool {
{
continue;
}
+
+ // Recursively process subdirectories
if path.is_dir() {
self.get_files_from_directory(
path.to_str()
@@ -547,9 +665,13 @@ impl MutationTool {
)?;
continue;
}
+
+ // Skip files with extensions other than "kt"
if path.extension() != Some("kt".as_ref()) {
continue;
}
+
+ // Skip files in ignored directories
if path.components().any(|p| {
self.kodekraken_config
.ignore
@@ -560,7 +682,10 @@ impl MutationTool {
}) {
continue;
}
+
let file_name = entry.file_name();
+
+ // Skip files matching the specified regex patterns
if self
.kodekraken_config
.ignore
@@ -578,6 +703,8 @@ impl MutationTool {
{
continue;
}
+
+ // Add the path to the list of existing files
existing_files.push(
path.to_str()
.ok_or(KodeKrakenError::Error(
@@ -591,14 +718,44 @@ impl MutationTool {
}
}
+fn create_mutation_chucks(file_mutations: &HashMap) -> Vec> {
+ // Merge all mutants into one vector
+ let all_mutations: Vec = file_mutations
+ .values()
+ .flat_map(|fm| fm.mutations.clone())
+ .collect();
+
+ // Partition the mutations into chunks
+ let chunk_size = ((all_mutations.len() as f32) / MAX_BUILD_THREADS).ceil() as usize;
+ let chunks: Vec> = all_mutations
+ .chunks(chunk_size)
+ .map(|c| c.to_vec())
+ .collect();
+ chunks
+}
+
+fn create_progress_bar(num_mutations: usize) -> Result> {
+ let progress_bar = Arc::new(ProgressBar::new(num_mutations as u64));
+ progress_bar.set_style(
+ ProgressStyle::default_bar()
+ .template(
+ "[{elapsed_precise}] {bar:40.cyan/blue} {pos:>7}/{len:7} {msg} - Running tests...",
+ )
+ .map_err(|e| KodeKrakenError::Error(e.to_string()))?
+ .progress_chars("=> "),
+ );
+ Ok(progress_bar)
+}
+
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use uuid::Uuid;
+ use crate::mutation_tool::test_util::*;
+
use super::*;
- use crate::test_config::*;
fn create_temp_directory(file_contents: &str) -> (Uuid, String) {
let mutation_test_id = Uuid::new_v4();
@@ -691,13 +848,23 @@ mod tests {
remove_directory(mutation_test_id);
}
+ // #[test]
+ // fn test_tool_exists_if_no_mutable_files_found() {
+ // todo!()
+ // }
+
+ // #[test]
+ // fn test_tool_exists_if_no_mutations_are_made() {
+ // todo!()
+ // }
+
#[test]
fn test_mutate_arithmetic_mutated_files_exist() {
let (mutation_test_id, output_directory) = create_temp_directory(KOTLIN_TEST_CODE);
let mut mutator = create_mutator_with_specific_operators(
mutation_test_id,
output_directory.clone(),
- vec![MutationOperators::ArthimeticOperator],
+ vec![MutationOperators::ArithmeticReplacementOperator],
);
assert_all_mutation_files_were_created(&mut mutator, mutation_test_id, output_directory);
}
@@ -708,7 +875,7 @@ mod tests {
let mut mutator = create_mutator_with_specific_operators(
mutation_test_id,
output_directory.clone(),
- vec![MutationOperators::ArthimeticOperator],
+ vec![MutationOperators::ArithmeticReplacementOperator],
);
assert_all_mutations_are_correct(&mut mutator, mutation_test_id, output_directory);
}
@@ -720,7 +887,7 @@ mod tests {
let mut mutator = create_mutator_with_specific_operators(
mutation_test_id,
output_directory.clone(),
- vec![MutationOperators::AssignmentOperator],
+ vec![MutationOperators::AssignmentReplacementOperator],
);
assert_all_mutation_files_were_created(&mut mutator, mutation_test_id, output_directory);
}
@@ -732,7 +899,7 @@ mod tests {
let mut mutator = create_mutator_with_specific_operators(
mutation_test_id,
output_directory.clone(),
- vec![MutationOperators::AssignmentOperator],
+ vec![MutationOperators::AssignmentReplacementOperator],
);
assert_all_mutations_are_correct(&mut mutator, mutation_test_id, output_directory);
}
@@ -743,7 +910,7 @@ mod tests {
let mut mutator = create_mutator_with_specific_operators(
mutation_test_id,
output_directory.clone(),
- vec![MutationOperators::LogicalOperator],
+ vec![MutationOperators::LogicalReplacementOperator],
);
assert_all_mutation_files_were_created(&mut mutator, mutation_test_id, output_directory);
}
@@ -754,7 +921,7 @@ mod tests {
let mut mutator = create_mutator_with_specific_operators(
mutation_test_id,
output_directory.clone(),
- vec![MutationOperators::LogicalOperator],
+ vec![MutationOperators::LogicalReplacementOperator],
);
assert_all_mutations_are_correct(&mut mutator, mutation_test_id, output_directory);
}
@@ -766,7 +933,7 @@ mod tests {
let mut mutator = create_mutator_with_specific_operators(
mutation_test_id,
output_directory.clone(),
- vec![MutationOperators::RelationalOperator],
+ vec![MutationOperators::RelationalReplacementOperator],
);
assert_all_mutation_files_were_created(&mut mutator, mutation_test_id, output_directory);
}
@@ -778,7 +945,7 @@ mod tests {
let mut mutator = create_mutator_with_specific_operators(
mutation_test_id,
output_directory.clone(),
- vec![MutationOperators::RelationalOperator],
+ vec![MutationOperators::RelationalReplacementOperator],
);
assert_all_mutations_are_correct(&mut mutator, mutation_test_id, output_directory);
}
@@ -789,7 +956,7 @@ mod tests {
let mut mutator = create_mutator_with_specific_operators(
mutation_test_id,
output_directory.clone(),
- vec![MutationOperators::UnaryOperator],
+ vec![MutationOperators::UnaryReplacementOperator],
);
assert_all_mutation_files_were_created(&mut mutator, mutation_test_id, output_directory);
}
@@ -800,7 +967,7 @@ mod tests {
let mut mutator = create_mutator_with_specific_operators(
mutation_test_id,
output_directory.clone(),
- vec![MutationOperators::UnaryOperator],
+ vec![MutationOperators::UnaryReplacementOperator],
);
assert_all_mutations_are_correct(&mut mutator, mutation_test_id, output_directory);
}
@@ -828,4 +995,19 @@ mod tests {
);
assert_all_mutations_are_correct(&mut mutator, mutation_test_id, output_directory);
}
+
+ // #[test]
+ // fn test_mutate_no_null_assertion_mutations_are_correct() {
+ // todo!()
+ // }
+
+ // #[test]
+ // fn test_mutate_elvis_remove_mutations_are_correct() {
+ // todo!()
+ // }
+
+ // #[test]
+ // fn test_mutate_elvis_literal_change_mutations_are_correct() {
+ // todo!()
+ // }
}
diff --git a/src/test_config/mod.rs b/src/test_config/mod.rs
deleted file mode 100644
index 327392e..0000000
--- a/src/test_config/mod.rs
+++ /dev/null
@@ -1,64 +0,0 @@
-pub const KOTLIN_TEST_CODE: &str = r#"
-fun main() {
- // Arithmetic expressions
- val a = 10
- val b = 3
- val c = a + b
- val d = a - b
- val e = a * b
- val f = a / b
- val g = a % b
-}
-"#;
-
-pub const KOTLIN_RELATIONAL_TEST_CODE: &str = r#"
-fun main() {
- // Relational expressions
- val a = 10
- val b = 3
- val c = a > b
- val d = a < b
- val e = a >= b
- val f = a <= b
- val g = a == b
- val h = a != b
-}
-"#;
-
-pub const KOTLIN_LOGICAL_TEST_CODE: &str = r#"
-fun main() {
- // Logical expressions
- val a = true
- val b = false
- val c = a && b
- val d = a || b
-}
-"#;
-
-pub const KOTLIN_UNARY_TEST_CODE: &str = r#"
-var h = 5
-h++
-h--
-++h
---h
-"#;
-
-pub const KOTLIN_UNARY_REMOVAL_TEST_CODE: &str = r#"
-var h = 5
-h++
-h--
-++h
---h
-val a = !h
-val b = -h
-val c = +h
-"#;
-
-pub const KOTLIN_ASSIGNMENT_TEST_CODE: &str = r#"
-var h = 5
-h += 3
-h -= 1
-h *= 2
-h /= 4
-h %= 2
-"#;
diff --git a/tests/kode_kraken/main.rs b/tests/kode_kraken/main.rs
new file mode 100644
index 0000000..9359206
--- /dev/null
+++ b/tests/kode_kraken/main.rs
@@ -0,0 +1,56 @@
+use assert_cmd::prelude::*;
+use kode_kraken::{config::KodeKrakenConfig, mutation_tool::MutationOperators};
+use std::{fs, path::Path, process::Command};
+
+#[test]
+fn test_tool_runs_correctly() {
+ // Set the config
+ let mut config = KodeKrakenConfig::default();
+ config.general.operators = vec![
+ MutationOperators::ArithmeticReplacementOperator,
+ MutationOperators::AssignmentReplacementOperator,
+ ];
+ // Create or replace the config file
+ fs::write(
+ "tests/kotlin-test-projects/demo/kodekraken.config.json",
+ serde_json::to_string(&config).unwrap(),
+ )
+ .unwrap();
+
+ let mut cmd = Command::cargo_bin("kode-kraken").expect("Could not find kode-kraken binary");
+
+ // Create command
+ cmd.arg("mutate").arg("tests/kotlin-test-projects/demo");
+
+ // Assert that the command runs successfully
+ cmd.assert().success();
+
+ let dir_path = Path::new("tests/kotlin-test-projects/demo/kode-kraken-dist");
+ // Assert that kode-kraken-dist was created
+ assert!(dir_path.exists());
+ // Assert that the backups directory was created and that the backup file exists
+ let backup_path = dir_path.join("backups");
+ assert!(backup_path.exists());
+ // Get the files in the backups directory
+ let backup_files = fs::read_dir(backup_path).unwrap();
+ // Get the files in the mutations directory
+ let real_files = fs::read_dir("tests/kotlin-test-projects/demo/src/main/kotlin").unwrap();
+ // Assert that the number of files in the backups directory is the same as the number of files in the mutations directory
+ assert_eq!(backup_files.count(), real_files.count());
+
+ // Assert that the logs directory was created and that the log file exists
+ let log_path = dir_path.join("logs");
+ assert!(log_path.exists());
+ assert!(log_path.join("kode-kraken.log").exists());
+ // Assert that the mutations directory was created and that the mutations file exists
+ let mutations_path = dir_path.join("mutations");
+ assert!(mutations_path.exists());
+ let mutation_files = fs::read_dir(mutations_path).unwrap();
+ assert!(mutation_files.count() > 0);
+ // Assert that the output.csv file was created
+ let output_path = dir_path.join("output.csv");
+ assert!(output_path.exists());
+ // Assert that the report.html file was created
+ let report_path = dir_path.join("report.html");
+ assert!(report_path.exists());
+}
diff --git a/kotlin-test-projects/demo/build.gradle.kts b/tests/kotlin-test-projects/demo/build.gradle.kts
similarity index 100%
rename from kotlin-test-projects/demo/build.gradle.kts
rename to tests/kotlin-test-projects/demo/build.gradle.kts
diff --git a/kotlin-test-projects/demo/gradle.properties b/tests/kotlin-test-projects/demo/gradle.properties
similarity index 100%
rename from kotlin-test-projects/demo/gradle.properties
rename to tests/kotlin-test-projects/demo/gradle.properties
diff --git a/kotlin-test-projects/demo/gradle/wrapper/gradle-wrapper.jar b/tests/kotlin-test-projects/demo/gradle/wrapper/gradle-wrapper.jar
similarity index 100%
rename from kotlin-test-projects/demo/gradle/wrapper/gradle-wrapper.jar
rename to tests/kotlin-test-projects/demo/gradle/wrapper/gradle-wrapper.jar
diff --git a/kotlin-test-projects/demo/gradle/wrapper/gradle-wrapper.properties b/tests/kotlin-test-projects/demo/gradle/wrapper/gradle-wrapper.properties
similarity index 100%
rename from kotlin-test-projects/demo/gradle/wrapper/gradle-wrapper.properties
rename to tests/kotlin-test-projects/demo/gradle/wrapper/gradle-wrapper.properties
diff --git a/kotlin-test-projects/demo/gradlew b/tests/kotlin-test-projects/demo/gradlew
similarity index 100%
rename from kotlin-test-projects/demo/gradlew
rename to tests/kotlin-test-projects/demo/gradlew
diff --git a/kotlin-test-projects/demo/gradlew.bat b/tests/kotlin-test-projects/demo/gradlew.bat
similarity index 100%
rename from kotlin-test-projects/demo/gradlew.bat
rename to tests/kotlin-test-projects/demo/gradlew.bat
diff --git a/tests/kotlin-test-projects/demo/kodekraken.config.json b/tests/kotlin-test-projects/demo/kodekraken.config.json
new file mode 100644
index 0000000..6cafeef
--- /dev/null
+++ b/tests/kotlin-test-projects/demo/kodekraken.config.json
@@ -0,0 +1 @@
+{"general":{"timeout":null,"operators":["ArithmeticReplacementOperator","AssignmentReplacementOperator"]},"ignore":{"ignore_files":["^.*Test\\.[^.]*$"],"ignore_directories":["dist","build","bin",".gradle",".idea","gradle"]},"threading":{"max_threads":30},"output":{"display_end_table":false},"logging":{"log_level":"info"}}
\ No newline at end of file
diff --git a/kotlin-test-projects/demo/settings.gradle.kts b/tests/kotlin-test-projects/demo/settings.gradle.kts
similarity index 100%
rename from kotlin-test-projects/demo/settings.gradle.kts
rename to tests/kotlin-test-projects/demo/settings.gradle.kts
diff --git a/kotlin-test-projects/demo/src/main/kotlin/Calculator.kt b/tests/kotlin-test-projects/demo/src/main/kotlin/Calculator.kt
similarity index 57%
rename from kotlin-test-projects/demo/src/main/kotlin/Calculator.kt
rename to tests/kotlin-test-projects/demo/src/main/kotlin/Calculator.kt
index ddbcbfa..cec6f6e 100644
--- a/kotlin-test-projects/demo/src/main/kotlin/Calculator.kt
+++ b/tests/kotlin-test-projects/demo/src/main/kotlin/Calculator.kt
@@ -16,4 +16,19 @@ class Calculator {
var result:Int? = test!!.length
return result
}
+
+ fun ElvisLiteralChangeOperator() {
+ var b: String? = null
+ var x = b?.length ?: -11
+ var y = b?.length ?: "Hello There"
+ var z = b?.length ?: 1.0
+ var a = b?.length ?: 1.0f
+ var c = b?.length ?: 1L
+
+ if (x > 1) {
+ println("Len is greater than 1!")
+ }
+
+ println(x)
+ }
}
diff --git a/kotlin-test-projects/demo/src/main/kotlin/NumberOperations.kt b/tests/kotlin-test-projects/demo/src/main/kotlin/NumberOperations.kt
similarity index 100%
rename from kotlin-test-projects/demo/src/main/kotlin/NumberOperations.kt
rename to tests/kotlin-test-projects/demo/src/main/kotlin/NumberOperations.kt
diff --git a/kotlin-test-projects/demo/src/test/kotlin/CalculatorTest.kt b/tests/kotlin-test-projects/demo/src/test/kotlin/CalculatorTest.kt
similarity index 100%
rename from kotlin-test-projects/demo/src/test/kotlin/CalculatorTest.kt
rename to tests/kotlin-test-projects/demo/src/test/kotlin/CalculatorTest.kt
diff --git a/kotlin-test-projects/demo/src/test/kotlin/NumberOperationTest.kt b/tests/kotlin-test-projects/demo/src/test/kotlin/NumberOperationTest.kt
similarity index 100%
rename from kotlin-test-projects/demo/src/test/kotlin/NumberOperationTest.kt
rename to tests/kotlin-test-projects/demo/src/test/kotlin/NumberOperationTest.kt
diff --git a/kotlin-test-projects/kotlin-project/build.gradle.kts b/tests/kotlin-test-projects/kotlin-project/build.gradle.kts
similarity index 100%
rename from kotlin-test-projects/kotlin-project/build.gradle.kts
rename to tests/kotlin-test-projects/kotlin-project/build.gradle.kts
diff --git a/kotlin-test-projects/kotlin-project/gradle.properties b/tests/kotlin-test-projects/kotlin-project/gradle.properties
similarity index 100%
rename from kotlin-test-projects/kotlin-project/gradle.properties
rename to tests/kotlin-test-projects/kotlin-project/gradle.properties
diff --git a/kotlin-test-projects/kotlin-project/gradle/wrapper/gradle-wrapper.jar b/tests/kotlin-test-projects/kotlin-project/gradle/wrapper/gradle-wrapper.jar
similarity index 100%
rename from kotlin-test-projects/kotlin-project/gradle/wrapper/gradle-wrapper.jar
rename to tests/kotlin-test-projects/kotlin-project/gradle/wrapper/gradle-wrapper.jar
diff --git a/kotlin-test-projects/kotlin-project/gradle/wrapper/gradle-wrapper.properties b/tests/kotlin-test-projects/kotlin-project/gradle/wrapper/gradle-wrapper.properties
similarity index 100%
rename from kotlin-test-projects/kotlin-project/gradle/wrapper/gradle-wrapper.properties
rename to tests/kotlin-test-projects/kotlin-project/gradle/wrapper/gradle-wrapper.properties
diff --git a/kotlin-test-projects/kotlin-project/gradlew b/tests/kotlin-test-projects/kotlin-project/gradlew
similarity index 100%
rename from kotlin-test-projects/kotlin-project/gradlew
rename to tests/kotlin-test-projects/kotlin-project/gradlew
diff --git a/kotlin-test-projects/kotlin-project/gradlew.bat b/tests/kotlin-test-projects/kotlin-project/gradlew.bat
similarity index 100%
rename from kotlin-test-projects/kotlin-project/gradlew.bat
rename to tests/kotlin-test-projects/kotlin-project/gradlew.bat
diff --git a/kotlin-test-projects/kotlin-project/settings.gradle.kts b/tests/kotlin-test-projects/kotlin-project/settings.gradle.kts
similarity index 100%
rename from kotlin-test-projects/kotlin-project/settings.gradle.kts
rename to tests/kotlin-test-projects/kotlin-project/settings.gradle.kts
diff --git a/kotlin-test-projects/kotlin-project/src/main/kotlin/Calculator.kt b/tests/kotlin-test-projects/kotlin-project/src/main/kotlin/Calculator.kt
similarity index 100%
rename from kotlin-test-projects/kotlin-project/src/main/kotlin/Calculator.kt
rename to tests/kotlin-test-projects/kotlin-project/src/main/kotlin/Calculator.kt
diff --git a/kotlin-test-projects/kotlin-project/src/main/kotlin/NumberOperations.kt b/tests/kotlin-test-projects/kotlin-project/src/main/kotlin/NumberOperations.kt
similarity index 100%
rename from kotlin-test-projects/kotlin-project/src/main/kotlin/NumberOperations.kt
rename to tests/kotlin-test-projects/kotlin-project/src/main/kotlin/NumberOperations.kt
diff --git a/kotlin-test-projects/kotlin-project/src/test/kotlin/CalculatorTest.kt b/tests/kotlin-test-projects/kotlin-project/src/test/kotlin/CalculatorTest.kt
similarity index 100%
rename from kotlin-test-projects/kotlin-project/src/test/kotlin/CalculatorTest.kt
rename to tests/kotlin-test-projects/kotlin-project/src/test/kotlin/CalculatorTest.kt
diff --git a/kotlin-test-projects/kotlin-project/src/test/kotlin/NumberOperationTest.kt b/tests/kotlin-test-projects/kotlin-project/src/test/kotlin/NumberOperationTest.kt
similarity index 100%
rename from kotlin-test-projects/kotlin-project/src/test/kotlin/NumberOperationTest.kt
rename to tests/kotlin-test-projects/kotlin-project/src/test/kotlin/NumberOperationTest.kt
diff --git a/kotlin-test-projects/mutations/BuildFails.kt b/tests/kotlin-test-projects/mutations/BuildFails.kt
similarity index 94%
rename from kotlin-test-projects/mutations/BuildFails.kt
rename to tests/kotlin-test-projects/mutations/BuildFails.kt
index b469653..5d4bf18 100644
--- a/kotlin-test-projects/mutations/BuildFails.kt
+++ b/tests/kotlin-test-projects/mutations/BuildFails.kt
@@ -20,7 +20,7 @@ class Calculator {
/**
AUTO GENERATED COMMENT
- Mutation Operator: UnaryOperator
+ Mutation Operator: UnaryReplacementOperator
Line number: 29
Id: eeea3a90-506d-49c7-b0ab-219fecc09ceb,
Old Operator: ++,
diff --git a/kotlin-test-projects/mutations/Killed.kt b/tests/kotlin-test-projects/mutations/Killed.kt
similarity index 94%
rename from kotlin-test-projects/mutations/Killed.kt
rename to tests/kotlin-test-projects/mutations/Killed.kt
index 3310703..ca36d9f 100644
--- a/kotlin-test-projects/mutations/Killed.kt
+++ b/tests/kotlin-test-projects/mutations/Killed.kt
@@ -23,7 +23,7 @@ class Calculator {
/**
AUTO GENERATED COMMENT
- Mutation Operator: AssignmentOperator
+ Mutation Operator: AssignmentReplacementOperator
Line number: 32
Id: e4f91eaa-6011-44cd-ae0b-0668eda62769,
Old Operator: *=,
diff --git a/kotlin-test-projects/mutations/Survived.kt b/tests/kotlin-test-projects/mutations/Survived.kt
similarity index 94%
rename from kotlin-test-projects/mutations/Survived.kt
rename to tests/kotlin-test-projects/mutations/Survived.kt
index 657d75a..405ee8c 100644
--- a/kotlin-test-projects/mutations/Survived.kt
+++ b/tests/kotlin-test-projects/mutations/Survived.kt
@@ -11,7 +11,7 @@ class Calculator {
/**
AUTO GENERATED COMMENT
- Mutation Operator: AssignmentOperator
+ Mutation Operator: AssignmentReplacementOperator
Line number: 20
Id: 7d99bafd-300e-4fb0-9527-524b808763f6,
Old Operator: =,
diff --git a/kotlin-test-projects/no-gradle-project/Calculator.kt b/tests/kotlin-test-projects/no-gradle-project/Calculator.kt
similarity index 100%
rename from kotlin-test-projects/no-gradle-project/Calculator.kt
rename to tests/kotlin-test-projects/no-gradle-project/Calculator.kt
diff --git a/kotlin-test-projects/no-gradle-project/Main.kt b/tests/kotlin-test-projects/no-gradle-project/Main.kt
similarity index 100%
rename from kotlin-test-projects/no-gradle-project/Main.kt
rename to tests/kotlin-test-projects/no-gradle-project/Main.kt