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 Results
Kode Kraken Results
File Name# of Mutations# Survived# KilledScore
file2200NaN%
file1200NaN%
\ 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