initial commit
This commit is contained in:
15 files changed
+2310
No files matched your search
@@ -0,0 +1 @@
|
||||
/target
|
||||
Generated
+563
@@ -0,0 +1,563 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a"
|
||||
dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11d8838454fda655dafd3accb2b6e2bea645b9e4078abe84a22ceb947235c5cc"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0"
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-deque"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d"
|
||||
dependencies = [
|
||||
"crossbeam-epoch",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-epoch"
|
||||
version = "0.9.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a"
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.155"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
|
||||
|
||||
[[package]]
|
||||
name = "libm"
|
||||
version = "0.2.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
|
||||
|
||||
[[package]]
|
||||
name = "mcps"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"rand",
|
||||
"rand_distr",
|
||||
"rayon",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"libm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
|
||||
dependencies = [
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.86"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_distr"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
"rand",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon"
|
||||
version = "1.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa"
|
||||
dependencies = [
|
||||
"either",
|
||||
"rayon-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon-core"
|
||||
version = "1.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2"
|
||||
dependencies = [
|
||||
"crossbeam-deque",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.38.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.207"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5665e14a49a4ea1b91029ba7d3bca9f299e1f7cfa194388ccc20f14743e784f2"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.207"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6aea2634c86b0e8ef2cfdc0c340baede54ec27b1e46febd7f80dffb2aa44a00e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.124"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "66ad62847a56b3dba58cc891acd13884b9c61138d330c0d7b6181713d4fce38d"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_yaml"
|
||||
version = "0.9.34+deprecated"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
"unsafe-libyaml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.74"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fceb41e3d546d0bd83421d3409b1460cc7444cd389341a4c880fe7a042cb3d7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"fastrand",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
||||
|
||||
[[package]]
|
||||
name = "unsafe-libyaml"
|
||||
version = "0.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.59.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.7.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.7.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
[package]
|
||||
name = "mcps"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[lib]
|
||||
name = "mcps"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "mcps"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
clap = "4.5.15"
|
||||
rand = "0.8.5"
|
||||
rand_distr = "0.4.3"
|
||||
rayon = "1.10.0"
|
||||
serde = { version = "1.0.207", features = ["derive"] }
|
||||
serde_json = "1.0.124"
|
||||
serde_yaml = "0.9.34"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.12.0"
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
{
|
||||
"num_workers": 8,
|
||||
"estimate_confidence": 0.8,
|
||||
"tasks": [
|
||||
{
|
||||
"id": "RequirementsGathering",
|
||||
"min_time": 10.0,
|
||||
"max_time": 30.0,
|
||||
"dependencies": []
|
||||
},
|
||||
{
|
||||
"id": "SystemDesign",
|
||||
"min_time": 15.0,
|
||||
"max_time": 45.0,
|
||||
"dependencies": [
|
||||
"RequirementsGathering"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "DatabaseDesign",
|
||||
"min_time": 10.0,
|
||||
"max_time": 35.0,
|
||||
"dependencies": [
|
||||
"SystemDesign"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "UIUXDesign",
|
||||
"min_time": 20.0,
|
||||
"max_time": 50.0,
|
||||
"dependencies": [
|
||||
"RequirementsGathering"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "FrontendDevelopment",
|
||||
"min_time": 25.0,
|
||||
"max_time": 60.0,
|
||||
"dependencies": [
|
||||
"UIUXDesign"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "BackendDevelopment",
|
||||
"min_time": 30.0,
|
||||
"max_time": 80.0,
|
||||
"dependencies": [
|
||||
"SystemDesign",
|
||||
"DatabaseDesign"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "APIDevelopment",
|
||||
"min_time": 20.0,
|
||||
"max_time": 60.0,
|
||||
"dependencies": [
|
||||
"BackendDevelopment"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "IntegrationTesting",
|
||||
"min_time": 15.0,
|
||||
"max_time": 45.0,
|
||||
"dependencies": [
|
||||
"FrontendDevelopment",
|
||||
"BackendDevelopment",
|
||||
"APIDevelopment"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "UserAcceptanceTesting",
|
||||
"min_time": 10.0,
|
||||
"max_time": 30.0,
|
||||
"dependencies": [
|
||||
"IntegrationTesting"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "PerformanceOptimization",
|
||||
"min_time": 10.0,
|
||||
"max_time": 40.0,
|
||||
"dependencies": [
|
||||
"IntegrationTesting"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "SecurityAudit",
|
||||
"min_time": 10.0,
|
||||
"max_time": 30.0,
|
||||
"dependencies": [
|
||||
"IntegrationTesting"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "FinalDeployment",
|
||||
"min_time": 5.0,
|
||||
"max_time": 15.0,
|
||||
"dependencies": [
|
||||
"UserAcceptanceTesting",
|
||||
"PerformanceOptimization",
|
||||
"SecurityAudit"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "ProjectRetrospective",
|
||||
"min_time": 3.0,
|
||||
"max_time": 10.0,
|
||||
"dependencies": [
|
||||
"FinalDeployment"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+205
@@ -0,0 +1,205 @@
|
||||
num_workers: 5
|
||||
estimate_confidence: 0.80
|
||||
tasks:
|
||||
# Project Initiation and Planning
|
||||
- id: project_kickoff
|
||||
min_time: 2
|
||||
max_time: 3
|
||||
dependencies: []
|
||||
|
||||
- id: requirements_gathering
|
||||
min_time: 10
|
||||
max_time: 15
|
||||
dependencies: [project_kickoff]
|
||||
|
||||
- id: system_architecture_design
|
||||
min_time: 15
|
||||
max_time: 20
|
||||
dependencies: [requirements_gathering]
|
||||
|
||||
- id: database_design
|
||||
min_time: 5
|
||||
max_time: 10
|
||||
dependencies: [system_architecture_design]
|
||||
|
||||
# Frontend Development
|
||||
- id: ui_ux_design
|
||||
min_time: 20
|
||||
max_time: 30
|
||||
dependencies: [requirements_gathering]
|
||||
|
||||
- id: frontend_setup
|
||||
min_time: 3
|
||||
max_time: 5
|
||||
dependencies: [ui_ux_design]
|
||||
|
||||
- id: user_authentication_frontend
|
||||
min_time: 5
|
||||
max_time: 10
|
||||
dependencies: [frontend_setup]
|
||||
|
||||
- id: product_catalog_frontend
|
||||
min_time: 15
|
||||
max_time: 25
|
||||
dependencies: [frontend_setup]
|
||||
|
||||
- id: shopping_cart_frontend
|
||||
min_time: 10
|
||||
max_time: 15
|
||||
dependencies: [frontend_setup]
|
||||
|
||||
- id: checkout_process_frontend
|
||||
min_time: 15
|
||||
max_time: 20
|
||||
dependencies: [shopping_cart_frontend]
|
||||
|
||||
- id: user_profile_frontend
|
||||
min_time: 5
|
||||
max_time: 10
|
||||
dependencies: [user_authentication_frontend]
|
||||
|
||||
- id: search_functionality_frontend
|
||||
min_time: 10
|
||||
max_time: 15
|
||||
dependencies: [product_catalog_frontend]
|
||||
|
||||
# Backend Development
|
||||
- id: backend_setup
|
||||
min_time: 5
|
||||
max_time: 10
|
||||
dependencies: [system_architecture_design, database_design]
|
||||
|
||||
- id: user_authentication_backend
|
||||
min_time: 10
|
||||
max_time: 15
|
||||
dependencies: [backend_setup]
|
||||
|
||||
- id: product_catalog_backend
|
||||
min_time: 20
|
||||
max_time: 30
|
||||
dependencies: [backend_setup]
|
||||
|
||||
- id: shopping_cart_backend
|
||||
min_time: 15
|
||||
max_time: 20
|
||||
dependencies: [backend_setup]
|
||||
|
||||
- id: checkout_process_backend
|
||||
min_time: 20
|
||||
max_time: 25
|
||||
dependencies: [shopping_cart_backend]
|
||||
|
||||
- id: user_profile_backend
|
||||
min_time: 10
|
||||
max_time: 15
|
||||
dependencies: [user_authentication_backend]
|
||||
|
||||
- id: search_functionality_backend
|
||||
min_time: 15
|
||||
max_time: 20
|
||||
dependencies: [product_catalog_backend]
|
||||
|
||||
- id: payment_gateway_integration
|
||||
min_time: 10
|
||||
max_time: 15
|
||||
dependencies: [checkout_process_backend]
|
||||
|
||||
# API Development and Integration
|
||||
- id: api_design
|
||||
min_time: 5
|
||||
max_time: 10
|
||||
dependencies: [system_architecture_design]
|
||||
|
||||
- id: api_development
|
||||
min_time: 25
|
||||
max_time: 35
|
||||
dependencies: [api_design, backend_setup]
|
||||
|
||||
- id: api_documentation
|
||||
min_time: 5
|
||||
max_time: 10
|
||||
dependencies: [api_development]
|
||||
|
||||
# Testing
|
||||
- id: unit_testing
|
||||
min_time: 20
|
||||
max_time: 30
|
||||
dependencies:
|
||||
[
|
||||
user_authentication_backend,
|
||||
product_catalog_backend,
|
||||
shopping_cart_backend,
|
||||
checkout_process_backend,
|
||||
user_profile_backend,
|
||||
search_functionality_backend,
|
||||
]
|
||||
|
||||
- id: integration_testing
|
||||
min_time: 15
|
||||
max_time: 20
|
||||
dependencies: [unit_testing, api_development]
|
||||
|
||||
- id: user_acceptance_testing
|
||||
min_time: 10
|
||||
max_time: 15
|
||||
dependencies: [integration_testing]
|
||||
|
||||
# Security and Performance
|
||||
- id: security_audit
|
||||
min_time: 10
|
||||
max_time: 15
|
||||
dependencies: [integration_testing]
|
||||
|
||||
- id: performance_optimization
|
||||
min_time: 15
|
||||
max_time: 20
|
||||
dependencies: [integration_testing]
|
||||
|
||||
# Deployment and Launch
|
||||
- id: deployment_planning
|
||||
min_time: 5
|
||||
max_time: 10
|
||||
dependencies: [system_architecture_design]
|
||||
|
||||
- id: staging_environment_setup
|
||||
min_time: 5
|
||||
max_time: 10
|
||||
dependencies: [deployment_planning]
|
||||
|
||||
- id: production_environment_setup
|
||||
min_time: 10
|
||||
max_time: 15
|
||||
dependencies: [staging_environment_setup]
|
||||
|
||||
- id: data_migration
|
||||
min_time: 10
|
||||
max_time: 20
|
||||
dependencies: [production_environment_setup]
|
||||
|
||||
- id: user_training
|
||||
min_time: 5
|
||||
max_time: 10
|
||||
dependencies: [user_acceptance_testing]
|
||||
|
||||
- id: final_deployment
|
||||
min_time: 5
|
||||
max_time: 10
|
||||
dependencies:
|
||||
[
|
||||
user_acceptance_testing,
|
||||
security_audit,
|
||||
performance_optimization,
|
||||
data_migration,
|
||||
user_training,
|
||||
]
|
||||
|
||||
# Post-launch
|
||||
- id: post_launch_support
|
||||
min_time: 10
|
||||
max_time: 15
|
||||
dependencies: [final_deployment]
|
||||
|
||||
- id: project_retrospective
|
||||
min_time: 2
|
||||
max_time: 3
|
||||
dependencies: [post_launch_support]
|
||||
@@ -0,0 +1,4 @@
|
||||
pub mod schedule;
|
||||
pub mod schedule_loader;
|
||||
pub mod simulation;
|
||||
pub mod task;
|
||||
+203
@@ -0,0 +1,203 @@
|
||||
use mcps::{schedule::Schedule, simulation::run_multiple_simulations};
|
||||
|
||||
use clap::{Arg, Command};
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
const AFTER_HELP_TEXT: &str = "\
|
||||
Examples of supported file formats:
|
||||
|
||||
YAML Format:
|
||||
|
||||
num_workers: 5
|
||||
estimate_confidence: 0.80
|
||||
tasks:
|
||||
- id: DesignPhase
|
||||
min_time: 1.5 # 1.5 days
|
||||
max_time: 3.5 # 3.5 days
|
||||
dependencies: []
|
||||
- id: ImplementationPhase
|
||||
min_time: 2.25 # 2.25 days
|
||||
max_time: 4.75 # 4.75 days
|
||||
dependencies: [DesignPhase]
|
||||
|
||||
JSON Format:
|
||||
|
||||
{
|
||||
\"num_workers\": 5,
|
||||
\"estimate_confidence\": 0.80,
|
||||
\"tasks\": [
|
||||
{
|
||||
\"id\": \"DesignPhase\",
|
||||
\"min_time\": 1.5,
|
||||
\"max_time\": 3.5,
|
||||
\"dependencies\": []
|
||||
},
|
||||
{
|
||||
\"id\": \"ImplementationPhase\",
|
||||
\"min_time\": 2.25,
|
||||
\"max_time\": 4.75,
|
||||
\"dependencies\": [\"DesignPhase\"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Note: `estimate_confidence` is the confidence (0.0,1.0) that the
|
||||
actual values fall in the task estimate ranges. For example,
|
||||
0.8 means you believe that the actual time it will take to
|
||||
complete a task falls in your [min_time,max_time] estimate
|
||||
range for that task 80% of the time.";
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let matches = Command::new("Monte Carlo Project Scheduler")
|
||||
.version("1.0")
|
||||
.author("Stephen Waits <steve@waits.net>")
|
||||
.about("Runs Monte Carlo simulations on project schedules")
|
||||
.arg(
|
||||
Arg::new("filename")
|
||||
.help("Path to the schedule file (.yaml or .json).")
|
||||
.required(true)
|
||||
.index(1),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("iterations")
|
||||
.short('i')
|
||||
.long("iterations")
|
||||
.help("Number of iterations to run")
|
||||
.default_value("50000"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("workers")
|
||||
.short('w')
|
||||
.long("workers")
|
||||
.help("Override number of workers specified in schedule file")
|
||||
.value_name("num_workers"),
|
||||
)
|
||||
.after_help(AFTER_HELP_TEXT)
|
||||
.get_matches();
|
||||
|
||||
let schedule_path = matches.get_one::<String>("filename").unwrap();
|
||||
let num_simulations: usize = matches.get_one::<String>("iterations").unwrap().parse()?;
|
||||
|
||||
// Ensure iterations is >= 100
|
||||
if num_simulations < 100 {
|
||||
return Err("Iterations must be at least 100.".into());
|
||||
}
|
||||
|
||||
// Load the schedule
|
||||
let mut schedule = Schedule::from_file(schedule_path)?;
|
||||
|
||||
// Check if workers are overridden by command-line argument
|
||||
if let Some(workers_str) = matches.get_one::<String>("workers") {
|
||||
let workers: usize = workers_str.parse()?;
|
||||
if workers < 1 {
|
||||
return Err("Invalid number of workers, must be 1 or more".into());
|
||||
}
|
||||
schedule.num_workers = workers;
|
||||
}
|
||||
|
||||
// Monte Carlo simulation
|
||||
let (project_durations, effort_times) = run_multiple_simulations(&schedule, num_simulations);
|
||||
|
||||
// Results output
|
||||
print_ascii_cdf(
|
||||
&project_durations,
|
||||
"Project Completion Time (by probability)",
|
||||
"Duration",
|
||||
);
|
||||
println!();
|
||||
print_ascii_cdf(
|
||||
&effort_times,
|
||||
"Total Work Effort (by probability)",
|
||||
"Effort",
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_ascii_cdf(data: &[Duration], title: &str, units: &str) {
|
||||
let mut sorted_data = data.to_vec();
|
||||
sorted_data.sort_unstable();
|
||||
let min = sorted_data[0];
|
||||
let max = *sorted_data.last().unwrap();
|
||||
|
||||
let width = 60; // Total width of the field
|
||||
|
||||
// Calculate padding
|
||||
let padding = (width - title.len()) / 2;
|
||||
|
||||
// Create the centered title with padding on both sides
|
||||
let centered_title = format!(
|
||||
"{:padding_left$}{}{:padding_right$}",
|
||||
"",
|
||||
title,
|
||||
"",
|
||||
padding_left = padding,
|
||||
padding_right = width - padding - title.len()
|
||||
);
|
||||
|
||||
println!("────┬────────────────────────────────────────────────────────────┬──────────");
|
||||
println!("%ile│{}│{:>10}", centered_title, units);
|
||||
println!("────┼────────────────────────────────────────────────────────────┼──────────");
|
||||
|
||||
let bar_width = 60;
|
||||
|
||||
for i in 0..=20 {
|
||||
let percentile = 100 - i * 5;
|
||||
let lower_percentile = if i == 20 {
|
||||
0.0000000001
|
||||
} else {
|
||||
(95 - i * 5) as f64 / 100.0
|
||||
};
|
||||
let upper_percentile = if i == 0 {
|
||||
0.9999999999
|
||||
} else {
|
||||
(100 - i * 5) as f64 / 100.0
|
||||
};
|
||||
let lower_index = (lower_percentile * (sorted_data.len() - 1) as f64).round() as usize;
|
||||
let upper_index = (upper_percentile * (sorted_data.len() - 1) as f64).round() as usize;
|
||||
let mid_duration = (sorted_data[lower_index] + sorted_data[upper_index]) / 2;
|
||||
let days = mid_duration.as_secs_f64() / 86400.0;
|
||||
let normalized_position = (days - min.as_secs_f64() / 86400.0)
|
||||
/ (max.as_secs_f64() / 86400.0 - min.as_secs_f64() / 86400.0);
|
||||
let bar_position = (normalized_position * bar_width as f64).round() as usize;
|
||||
|
||||
let (fg, bg) = if i % 2 == 0 {
|
||||
('░', '▓')
|
||||
} else {
|
||||
('▒', '█')
|
||||
};
|
||||
|
||||
let color_code = match percentile {
|
||||
0..=50 => "\x1b[31m", // Red
|
||||
55..=75 => "\x1b[33m", // Orange
|
||||
80..=90 => "\x1b[32m", // Green
|
||||
95..=95 => "\x1b[33m", // Orange
|
||||
100..=100 => "\x1b[31m", // Red
|
||||
_ => "\x1b[0m", // Default (shouldn't happen)
|
||||
};
|
||||
let reset_code = "\x1b[0m";
|
||||
|
||||
let bar_with_divider: String = (0..bar_width)
|
||||
.map(|j| match j {
|
||||
_ if j == bar_position => '▮',
|
||||
_ if j < bar_position => fg,
|
||||
_ => bg,
|
||||
})
|
||||
.collect();
|
||||
|
||||
println!(
|
||||
"{}{:>4}{}│{}{}{}│{}{:5.0} days{}",
|
||||
color_code,
|
||||
format!("p{}", percentile),
|
||||
reset_code,
|
||||
color_code,
|
||||
bar_with_divider,
|
||||
reset_code,
|
||||
color_code,
|
||||
days,
|
||||
reset_code
|
||||
);
|
||||
}
|
||||
println!("────┴────────────────────────────────────────────────────────────┴──────────");
|
||||
}
|
||||
+221
@@ -0,0 +1,221 @@
|
||||
use crate::task::Task;
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::time::Duration;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Schedule {
|
||||
pub tasks: Vec<Task>,
|
||||
pub num_workers: usize,
|
||||
pub estimate_confidence: f64,
|
||||
}
|
||||
|
||||
impl Schedule {
|
||||
pub fn new(
|
||||
tasks: Vec<Task>,
|
||||
num_workers: usize,
|
||||
estimate_confidence: f64,
|
||||
) -> Result<Self, String> {
|
||||
let schedule = Schedule {
|
||||
tasks,
|
||||
num_workers,
|
||||
estimate_confidence,
|
||||
};
|
||||
schedule.validate()?;
|
||||
Ok(schedule)
|
||||
}
|
||||
|
||||
pub fn validate(&self) -> Result<(), String> {
|
||||
if self.tasks.is_empty() {
|
||||
return Err("Empty task list".to_string());
|
||||
}
|
||||
if self.num_workers == 0 {
|
||||
return Err("Invalid number of workers (must be 1 or more)".to_string());
|
||||
}
|
||||
let all_task_ids: HashSet<&String> = self.tasks.iter().map(|t| &t.id).collect();
|
||||
for task in &self.tasks {
|
||||
if task.min_time > task.max_time {
|
||||
return Err(format!(
|
||||
"Minimum duration greater than maximum for task {}",
|
||||
task.id
|
||||
));
|
||||
}
|
||||
if task.min_time <= Duration::from_secs(0) || task.max_time <= Duration::from_secs(0) {
|
||||
return Err(format!("Invalid task duration for task {}", task.id));
|
||||
}
|
||||
for dep in &task.dependencies {
|
||||
if !all_task_ids.contains(dep) {
|
||||
return Err(format!("Missing dependency {} for task {}", dep, task.id));
|
||||
}
|
||||
}
|
||||
}
|
||||
self.check_cyclic_dependencies()
|
||||
}
|
||||
|
||||
fn check_cyclic_dependencies(&self) -> Result<(), String> {
|
||||
let mut visited = HashSet::new();
|
||||
let mut stack = HashSet::new();
|
||||
let task_map: HashMap<_, _> = self.tasks.iter().map(|t| (&t.id, t)).collect();
|
||||
|
||||
for task in &self.tasks {
|
||||
Self::dfs(&task.id, &task_map, &mut visited, &mut stack)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn dfs<'a>(
|
||||
task_id: &'a str,
|
||||
task_map: &'a HashMap<&'a String, &'a Task>,
|
||||
visited: &mut HashSet<&'a str>,
|
||||
stack: &mut HashSet<&'a str>,
|
||||
) -> Result<(), String> {
|
||||
if stack.contains(task_id) {
|
||||
return Err(format!(
|
||||
"Cyclic dependency detected involving task {}",
|
||||
task_id
|
||||
));
|
||||
}
|
||||
if visited.contains(task_id) {
|
||||
return Ok(());
|
||||
}
|
||||
visited.insert(task_id);
|
||||
stack.insert(task_id);
|
||||
if let Some(task) = task_map.get(&task_id.to_string()) {
|
||||
for dep in &task.dependencies {
|
||||
Self::dfs(dep, task_map, visited, stack)?;
|
||||
}
|
||||
}
|
||||
stack.remove(task_id);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn create_task(id: &str, min: u64, max: u64, deps: Vec<&str>) -> Task {
|
||||
Task {
|
||||
id: id.to_string(),
|
||||
dependencies: deps.into_iter().map(String::from).collect(),
|
||||
min_time: Duration::from_secs(min),
|
||||
max_time: Duration::from_secs(max),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_valid_schedule() {
|
||||
let tasks = vec![
|
||||
create_task("A", 1, 3, vec![]),
|
||||
create_task("B", 2, 4, vec!["A"]),
|
||||
create_task("C", 3, 5, vec!["A"]),
|
||||
];
|
||||
let schedule = Schedule::new(tasks, 2, 0.8);
|
||||
assert!(schedule.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_task_list() {
|
||||
let schedule = Schedule::new(vec![], 2, 0.8);
|
||||
assert!(schedule.is_err());
|
||||
assert_eq!(schedule.unwrap_err(), "Empty task list");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_task_duration() {
|
||||
let tasks = vec![create_task("A", 3, 1, vec![])];
|
||||
let schedule = Schedule::new(tasks, 2, 0.8);
|
||||
assert!(schedule.is_err());
|
||||
assert_eq!(
|
||||
schedule.unwrap_err(),
|
||||
"Minimum duration greater than maximum for task A"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_zero_duration() {
|
||||
let tasks = vec![create_task("A", 0, 1, vec![])];
|
||||
let schedule = Schedule::new(tasks, 2, 0.8);
|
||||
assert!(schedule.is_err());
|
||||
assert_eq!(schedule.unwrap_err(), "Invalid task duration for task A");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_missing_dependency() {
|
||||
let tasks = vec![
|
||||
create_task("A", 1, 3, vec![]),
|
||||
create_task("B", 2, 4, vec!["C"]),
|
||||
];
|
||||
let schedule = Schedule::new(tasks, 2, 0.8);
|
||||
assert!(schedule.is_err());
|
||||
assert_eq!(schedule.unwrap_err(), "Missing dependency C for task B");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cyclic_dependency() {
|
||||
let tasks = vec![
|
||||
create_task("A", 1, 3, vec!["B"]),
|
||||
create_task("B", 2, 4, vec!["A"]),
|
||||
];
|
||||
let schedule = Schedule::new(tasks, 2, 0.8);
|
||||
assert!(schedule.is_err());
|
||||
assert!(schedule
|
||||
.unwrap_err()
|
||||
.starts_with("Cyclic dependency detected involving task"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_complex_valid_schedule() {
|
||||
let tasks = vec![
|
||||
create_task("A", 1, 3, vec![]),
|
||||
create_task("B", 2, 4, vec!["A"]),
|
||||
create_task("C", 3, 5, vec!["A"]),
|
||||
create_task("D", 2, 6, vec!["B", "C"]),
|
||||
create_task("E", 1, 2, vec!["A"]),
|
||||
create_task("F", 2, 4, vec!["D", "E"]),
|
||||
];
|
||||
let schedule = Schedule::new(tasks, 3, 0.9);
|
||||
assert!(schedule.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_complex_cyclic_dependency() {
|
||||
let tasks = vec![
|
||||
create_task("A", 1, 3, vec!["F"]),
|
||||
create_task("B", 2, 4, vec!["A"]),
|
||||
create_task("C", 3, 5, vec!["B"]),
|
||||
create_task("D", 2, 6, vec!["C"]),
|
||||
create_task("E", 1, 2, vec!["D"]),
|
||||
create_task("F", 2, 4, vec!["E"]),
|
||||
];
|
||||
let schedule = Schedule::new(tasks, 3, 0.9);
|
||||
assert!(
|
||||
schedule.is_err(),
|
||||
"Expected cyclic dependency error, but got Ok"
|
||||
);
|
||||
if let Err(err) = schedule {
|
||||
assert!(
|
||||
err.starts_with("Cyclic dependency detected"),
|
||||
"Unexpected error message: {}",
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_self_dependency() {
|
||||
let tasks = vec![create_task("A", 1, 3, vec!["A"])];
|
||||
let schedule = Schedule::new(tasks, 1, 0.8);
|
||||
assert!(
|
||||
schedule.is_err(),
|
||||
"Expected self-dependency error, but got Ok"
|
||||
);
|
||||
if let Err(err) = schedule {
|
||||
assert!(
|
||||
err.starts_with("Cyclic dependency detected"),
|
||||
"Unexpected error message: {}",
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
use crate::{schedule::Schedule, task::Task};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use std::{fs::File, io::Read, path::Path, time::Duration};
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
struct ScheduleInput {
|
||||
num_workers: usize,
|
||||
estimate_confidence: f64,
|
||||
tasks: Vec<TaskInput>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
struct TaskInput {
|
||||
id: String,
|
||||
min_time: f64,
|
||||
max_time: f64,
|
||||
dependencies: Vec<String>,
|
||||
}
|
||||
|
||||
enum FileFormat {
|
||||
Yaml,
|
||||
Json,
|
||||
}
|
||||
|
||||
impl Schedule {
|
||||
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let mut file = File::open(&path)?;
|
||||
let mut contents = String::new();
|
||||
file.read_to_string(&mut contents)?;
|
||||
|
||||
let format = Self::detect_format(&path)?;
|
||||
let input: ScheduleInput = match format {
|
||||
FileFormat::Yaml => serde_yaml::from_str(&contents)?,
|
||||
FileFormat::Json => serde_json::from_str(&contents)?,
|
||||
};
|
||||
|
||||
let tasks = input
|
||||
.tasks
|
||||
.into_iter()
|
||||
.map(|t| Task {
|
||||
id: t.id,
|
||||
min_time: Duration::from_secs_f64(t.min_time * 24.0 * 60.0 * 60.0),
|
||||
max_time: Duration::from_secs_f64(t.max_time * 24.0 * 60.0 * 60.0),
|
||||
dependencies: t.dependencies,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let schedule = Schedule::new(tasks, input.num_workers, input.estimate_confidence)?;
|
||||
|
||||
schedule.validate()?;
|
||||
|
||||
Ok(schedule)
|
||||
}
|
||||
|
||||
fn detect_format<P: AsRef<Path>>(path: P) -> Result<FileFormat, Box<dyn std::error::Error>> {
|
||||
let extension = path
|
||||
.as_ref()
|
||||
.extension()
|
||||
.and_then(std::ffi::OsStr::to_str)
|
||||
.ok_or("File has no extension")?
|
||||
.to_lowercase();
|
||||
|
||||
match extension.as_str() {
|
||||
"yaml" | "yml" => Ok(FileFormat::Yaml),
|
||||
"json" => Ok(FileFormat::Json),
|
||||
_ => Err("Unsupported file format. Use .yaml, .yml, or .json".into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::io::Write;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
fn create_temp_file(content: &str, extension: &str) -> (NamedTempFile, std::path::PathBuf) {
|
||||
let mut temp_file = NamedTempFile::new().unwrap();
|
||||
write!(temp_file, "{}", content).unwrap();
|
||||
let path = temp_file.path().to_owned();
|
||||
let new_path = path.with_extension(extension);
|
||||
std::fs::rename(&path, &new_path).unwrap();
|
||||
(temp_file, new_path)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_from_yaml() {
|
||||
let yaml_content = r#"
|
||||
num_workers: 3
|
||||
estimate_confidence: 0.9
|
||||
tasks:
|
||||
- id: A
|
||||
min_time: 1
|
||||
max_time: 3
|
||||
dependencies: []
|
||||
- id: B
|
||||
min_time: 2
|
||||
max_time: 4
|
||||
dependencies: [A]
|
||||
"#;
|
||||
let (_temp_file, path) = create_temp_file(yaml_content, "yaml");
|
||||
let schedule = Schedule::from_file(path).unwrap();
|
||||
assert_eq!(schedule.tasks.len(), 2);
|
||||
assert_eq!(schedule.num_workers, 3);
|
||||
assert_eq!(schedule.estimate_confidence, 0.9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_from_json() {
|
||||
let json_content = r#"
|
||||
{
|
||||
"num_workers": 3,
|
||||
"estimate_confidence": 0.9,
|
||||
"tasks": [
|
||||
{
|
||||
"id": "A",
|
||||
"min_time": 1,
|
||||
"max_time": 3,
|
||||
"dependencies": []
|
||||
},
|
||||
{
|
||||
"id": "B",
|
||||
"min_time": 2,
|
||||
"max_time": 4,
|
||||
"dependencies": ["A"]
|
||||
}
|
||||
]
|
||||
}
|
||||
"#;
|
||||
let (_temp_file, path) = create_temp_file(json_content, "json");
|
||||
let schedule = Schedule::from_file(path).unwrap();
|
||||
assert_eq!(schedule.tasks.len(), 2);
|
||||
assert_eq!(schedule.num_workers, 3);
|
||||
assert_eq!(schedule.estimate_confidence, 0.9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unsupported_format() {
|
||||
let content = "irrelevant content";
|
||||
let (_temp_file, path) = create_temp_file(content, "txt");
|
||||
let result = Schedule::from_file(path);
|
||||
assert!(result.is_err());
|
||||
assert!(result
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("Unsupported file format"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_schedule() {
|
||||
let yaml_content = r#"
|
||||
num_workers: 3
|
||||
estimate_confidence: 0.9
|
||||
tasks:
|
||||
- id: A
|
||||
min_time: 3
|
||||
max_time: 1 # Invalid: min_time > max_time
|
||||
dependencies: []
|
||||
"#;
|
||||
let (_temp_file, path) = create_temp_file(yaml_content, "yaml");
|
||||
let result = Schedule::from_file(path);
|
||||
assert!(result.is_err());
|
||||
assert!(result
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("Minimum duration greater than maximum for task A"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_with_floating_point_times_yaml() {
|
||||
let yaml_content = r#"
|
||||
num_workers: 3
|
||||
estimate_confidence: 0.9
|
||||
tasks:
|
||||
- id: A
|
||||
min_time: 1.5 # 1.5 days
|
||||
max_time: 3.5 # 3.5 days
|
||||
dependencies: []
|
||||
"#;
|
||||
let (_temp_file, path) = create_temp_file(yaml_content, "yaml");
|
||||
let schedule = Schedule::from_file(path).unwrap();
|
||||
|
||||
// Verify that there is one task and it has the correct times
|
||||
assert_eq!(schedule.tasks.len(), 1);
|
||||
assert_eq!(
|
||||
schedule.tasks[0].min_time.as_secs_f64(),
|
||||
1.5 * 24.0 * 60.0 * 60.0
|
||||
);
|
||||
assert_eq!(
|
||||
schedule.tasks[0].max_time.as_secs_f64(),
|
||||
3.5 * 24.0 * 60.0 * 60.0
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_with_floating_point_times_json() {
|
||||
let json_content = r#"
|
||||
{
|
||||
"num_workers": 3,
|
||||
"estimate_confidence": 0.9,
|
||||
"tasks": [
|
||||
{
|
||||
"id": "A",
|
||||
"min_time": 1.5,
|
||||
"max_time": 3.5,
|
||||
"dependencies": []
|
||||
}
|
||||
]
|
||||
}
|
||||
"#;
|
||||
let (_temp_file, path) = create_temp_file(json_content, "json");
|
||||
let schedule = Schedule::from_file(path).unwrap();
|
||||
|
||||
// Verify that there is one task and it has the correct times
|
||||
assert_eq!(schedule.tasks.len(), 1);
|
||||
assert_eq!(
|
||||
schedule.tasks[0].min_time.as_secs_f64(),
|
||||
1.5 * 24.0 * 60.0 * 60.0
|
||||
);
|
||||
assert_eq!(
|
||||
schedule.tasks[0].max_time.as_secs_f64(),
|
||||
3.5 * 24.0 * 60.0 * 60.0
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
use crate::schedule::Schedule;
|
||||
|
||||
use rand::prelude::*;
|
||||
use rand_distr::{Distribution, Normal};
|
||||
use rayon::prelude::*;
|
||||
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
pub struct SimulationResult {
|
||||
pub total_project_duration: Duration,
|
||||
pub total_effort_time: Duration,
|
||||
}
|
||||
|
||||
pub fn run_multiple_simulations(
|
||||
schedule: &Schedule,
|
||||
num_simulations: usize,
|
||||
) -> (Vec<Duration>, Vec<Duration>) {
|
||||
(0..num_simulations)
|
||||
.into_par_iter()
|
||||
.map(|_| {
|
||||
let result = run_simulation(schedule);
|
||||
(result.total_project_duration, result.total_effort_time)
|
||||
})
|
||||
.unzip()
|
||||
}
|
||||
|
||||
fn run_simulation(schedule: &Schedule) -> SimulationResult {
|
||||
let mut rng = thread_rng();
|
||||
|
||||
// Simulate task times
|
||||
let task_effort_times: HashMap<_, _> = schedule
|
||||
.tasks
|
||||
.iter()
|
||||
.map(|task| {
|
||||
let task_time = simulate_task_time(
|
||||
&mut rng,
|
||||
task.min_time,
|
||||
task.max_time,
|
||||
schedule.estimate_confidence,
|
||||
);
|
||||
(&task.id, task_time)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let total_effort_time: Duration = task_effort_times.values().sum();
|
||||
|
||||
// Pre-compute task dependencies and reverse dependencies
|
||||
let mut task_dependencies: HashMap<_, HashSet<_>> = HashMap::new();
|
||||
let mut reverse_dependencies: HashMap<_, Vec<_>> = HashMap::new();
|
||||
for task in &schedule.tasks {
|
||||
task_dependencies.insert(&task.id, task.dependencies.iter().collect());
|
||||
for dep in &task.dependencies {
|
||||
reverse_dependencies.entry(dep).or_default().push(&task.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize task queue with tasks that have no dependencies
|
||||
let mut task_queue: Vec<_> = schedule
|
||||
.tasks
|
||||
.iter()
|
||||
.filter(|t| t.dependencies.is_empty())
|
||||
.map(|t| &t.id)
|
||||
.collect();
|
||||
|
||||
let mut current_time = Duration::default();
|
||||
let mut completed_tasks = HashSet::new();
|
||||
let mut worker_finish_times = vec![Duration::default(); schedule.num_workers];
|
||||
|
||||
while !task_queue.is_empty() || completed_tasks.len() < schedule.tasks.len() {
|
||||
// Find all available workers
|
||||
let available_workers: Vec<_> = worker_finish_times
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|&(_, &time)| time <= current_time)
|
||||
.map(|(i, _)| i)
|
||||
.collect();
|
||||
|
||||
if !task_queue.is_empty() && !available_workers.is_empty() {
|
||||
for &worker in &available_workers {
|
||||
if task_queue.is_empty() {
|
||||
break;
|
||||
}
|
||||
// Randomly choose the next task to assign
|
||||
let task_index = rng.gen_range(0..task_queue.len());
|
||||
let task_id = task_queue.swap_remove(task_index);
|
||||
|
||||
let task_duration = task_effort_times[task_id];
|
||||
worker_finish_times[worker] = current_time + task_duration;
|
||||
completed_tasks.insert(task_id);
|
||||
|
||||
// Add newly available tasks to queue
|
||||
if let Some(dependent_tasks) = reverse_dependencies.get(task_id) {
|
||||
for &dep_task in dependent_tasks {
|
||||
if !completed_tasks.contains(dep_task)
|
||||
&& !task_queue.contains(&dep_task)
|
||||
&& task_dependencies[dep_task]
|
||||
.iter()
|
||||
.all(|dep| completed_tasks.contains(dep))
|
||||
{
|
||||
task_queue.push(dep_task);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Move time forward to the next event
|
||||
current_time = *worker_finish_times.iter().min().unwrap_or(¤t_time);
|
||||
}
|
||||
|
||||
SimulationResult {
|
||||
total_project_duration: *worker_finish_times.iter().max().unwrap_or(¤t_time),
|
||||
total_effort_time,
|
||||
}
|
||||
}
|
||||
|
||||
fn simulate_task_time(
|
||||
rng: &mut impl Rng,
|
||||
min_time: Duration,
|
||||
max_time: Duration,
|
||||
estimate_confidence: f64,
|
||||
) -> Duration {
|
||||
let min_secs = min_time.as_secs_f64();
|
||||
let max_secs = max_time.as_secs_f64();
|
||||
let mean = (min_secs + max_secs) / 2.0;
|
||||
|
||||
let z_score = (1.0 - (1.0 - estimate_confidence) / 2.0).sqrt() * 2.0;
|
||||
let std_dev = (max_secs - min_secs) / (2.0 * z_score);
|
||||
|
||||
let normal = Normal::new(mean, std_dev).unwrap();
|
||||
let sampled_secs = normal.sample(rng).clamp(min_secs, max_secs);
|
||||
|
||||
Duration::from_secs_f64(sampled_secs)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::task::Task;
|
||||
|
||||
#[test]
|
||||
fn test_simulate_task_time() {
|
||||
let mut rng = thread_rng();
|
||||
let min_time = Duration::from_secs(5);
|
||||
let max_time = Duration::from_secs(15);
|
||||
let estimate_confidence = 0.8;
|
||||
|
||||
for _ in 0..1000 {
|
||||
let time = simulate_task_time(&mut rng, min_time, max_time, estimate_confidence);
|
||||
assert!(
|
||||
time >= min_time && time <= max_time,
|
||||
"Simulated time should be within range"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_single_task_simulation() {
|
||||
let task = Task::new("A", vec![], Duration::from_secs(5), Duration::from_secs(15));
|
||||
let schedule = Schedule::new(vec![task], 1, 0.8).unwrap();
|
||||
|
||||
let result = run_simulation(&schedule);
|
||||
|
||||
assert_eq!(
|
||||
result.total_project_duration, result.total_effort_time,
|
||||
"For a single task, project duration should equal effort time"
|
||||
);
|
||||
assert!(
|
||||
result.total_project_duration >= Duration::from_secs(5)
|
||||
&& result.total_project_duration <= Duration::from_secs(15),
|
||||
"Project duration should be within the task's time range"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_independent_tasks() {
|
||||
let tasks = vec![
|
||||
Task::new("A", vec![], Duration::from_secs(5), Duration::from_secs(10)),
|
||||
Task::new("B", vec![], Duration::from_secs(7), Duration::from_secs(12)),
|
||||
Task::new("C", vec![], Duration::from_secs(3), Duration::from_secs(8)),
|
||||
];
|
||||
let schedule = Schedule::new(tasks, 2, 0.8).unwrap();
|
||||
|
||||
let result = run_simulation(&schedule);
|
||||
|
||||
assert!(
|
||||
result.total_project_duration <= result.total_effort_time,
|
||||
"Project duration should not exceed total effort time"
|
||||
);
|
||||
assert!(
|
||||
result.total_project_duration >= Duration::from_secs(7),
|
||||
"Project duration should be at least the longest minimum task duration"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tasks_with_dependencies() {
|
||||
let tasks = vec![
|
||||
Task::new("A", vec![], Duration::from_secs(5), Duration::from_secs(10)),
|
||||
Task::new(
|
||||
"B",
|
||||
vec!["A".to_string()],
|
||||
Duration::from_secs(7),
|
||||
Duration::from_secs(12),
|
||||
),
|
||||
Task::new(
|
||||
"C",
|
||||
vec!["A".to_string()],
|
||||
Duration::from_secs(3),
|
||||
Duration::from_secs(8),
|
||||
),
|
||||
Task::new(
|
||||
"D",
|
||||
vec!["B".to_string(), "C".to_string()],
|
||||
Duration::from_secs(4),
|
||||
Duration::from_secs(9),
|
||||
),
|
||||
];
|
||||
let schedule = Schedule::new(tasks, 2, 0.8).unwrap();
|
||||
|
||||
let result = run_simulation(&schedule);
|
||||
|
||||
assert!(
|
||||
result.total_project_duration >= Duration::from_secs(14),
|
||||
"Project duration should be at least the minimum critical path duration"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_simulations_consistency() {
|
||||
let tasks = vec![
|
||||
Task::new("A", vec![], Duration::from_secs(5), Duration::from_secs(10)),
|
||||
Task::new(
|
||||
"B",
|
||||
vec!["A".to_string()],
|
||||
Duration::from_secs(7),
|
||||
Duration::from_secs(12),
|
||||
),
|
||||
];
|
||||
let schedule = Schedule::new(tasks, 1, 0.8).unwrap();
|
||||
|
||||
let (durations, efforts) = run_multiple_simulations(&schedule, 1000);
|
||||
|
||||
assert_eq!(durations.len(), 1000, "Should run 1000 simulations");
|
||||
assert_eq!(efforts.len(), 1000, "Should run 1000 simulations");
|
||||
|
||||
let avg_duration: Duration = durations.iter().sum::<Duration>() / 1000;
|
||||
let avg_effort: Duration = efforts.iter().sum::<Duration>() / 1000;
|
||||
|
||||
assert!(
|
||||
avg_duration >= Duration::from_secs(12) && avg_duration <= Duration::from_secs(22),
|
||||
"Average duration should be within expected range"
|
||||
);
|
||||
assert!(
|
||||
avg_effort >= Duration::from_secs(12) && avg_effort <= Duration::from_secs(22),
|
||||
"Average effort should be within expected range"
|
||||
);
|
||||
}
|
||||
}
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
use std::time::Duration;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Task {
|
||||
pub id: String,
|
||||
pub dependencies: Vec<String>,
|
||||
pub min_time: Duration,
|
||||
pub max_time: Duration,
|
||||
}
|
||||
|
||||
impl Task {
|
||||
pub fn new(
|
||||
id: &str,
|
||||
dependencies: Vec<String>,
|
||||
min_time: Duration,
|
||||
max_time: Duration,
|
||||
) -> Self {
|
||||
Task {
|
||||
id: id.to_string(),
|
||||
dependencies,
|
||||
min_time,
|
||||
max_time,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn days_to_duration(days: f64) -> Duration {
|
||||
Duration::from_secs_f64(days * 24.0 * 60.0 * 60.0)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const EPSILON: f64 = 1e-6;
|
||||
|
||||
fn assert_duration_eq(actual: Duration, expected: Duration) {
|
||||
let actual_secs = actual.as_secs_f64();
|
||||
let expected_secs = expected.as_secs_f64();
|
||||
assert!(
|
||||
(actual_secs - expected_secs).abs() < EPSILON,
|
||||
"Durations are not equal: actual = {:?}, expected = {:?}",
|
||||
actual,
|
||||
expected
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_task_creation() {
|
||||
let task = Task::new(
|
||||
"Task1",
|
||||
vec!["Dep1".to_string(), "Dep2".to_string()],
|
||||
Duration::from_secs(1),
|
||||
Duration::from_secs(5),
|
||||
);
|
||||
|
||||
assert_eq!(task.id, "Task1");
|
||||
assert_eq!(task.dependencies, vec!["Dep1", "Dep2"]);
|
||||
assert_eq!(task.min_time, Duration::from_secs(1));
|
||||
assert_eq!(task.max_time, Duration::from_secs(5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_task_creation_empty_dependencies() {
|
||||
let task = Task::new(
|
||||
"Task2",
|
||||
vec![],
|
||||
Duration::from_secs(2),
|
||||
Duration::from_secs(10),
|
||||
);
|
||||
|
||||
assert_eq!(task.id, "Task2");
|
||||
assert!(task.dependencies.is_empty());
|
||||
assert_eq!(task.min_time, Duration::from_secs(2));
|
||||
assert_eq!(task.max_time, Duration::from_secs(10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_task_creation_same_min_max_time() {
|
||||
let task = Task::new(
|
||||
"Task3",
|
||||
vec!["Dep3".to_string()],
|
||||
Duration::from_secs(5),
|
||||
Duration::from_secs(5),
|
||||
);
|
||||
|
||||
assert_eq!(task.id, "Task3");
|
||||
assert_eq!(task.dependencies, vec!["Dep3"]);
|
||||
assert_eq!(task.min_time, task.max_time);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_task_clone() {
|
||||
let task1 = Task::new(
|
||||
"Task4",
|
||||
vec!["Dep4".to_string()],
|
||||
Duration::from_secs(3),
|
||||
Duration::from_secs(7),
|
||||
);
|
||||
let task2 = task1.clone();
|
||||
|
||||
assert_eq!(task1.id, task2.id);
|
||||
assert_eq!(task1.dependencies, task2.dependencies);
|
||||
assert_eq!(task1.min_time, task2.min_time);
|
||||
assert_eq!(task1.max_time, task2.max_time);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_days_to_duration() {
|
||||
assert_duration_eq(days_to_duration(1.0), Duration::from_secs(24 * 60 * 60));
|
||||
assert_duration_eq(days_to_duration(7.0), Duration::from_secs(7 * 24 * 60 * 60));
|
||||
assert_duration_eq(days_to_duration(0.0), Duration::from_secs(0));
|
||||
assert_duration_eq(
|
||||
days_to_duration(365.0),
|
||||
Duration::from_secs(365 * 24 * 60 * 60),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_days_to_duration_fractional() {
|
||||
assert_duration_eq(days_to_duration(0.5), Duration::from_secs(12 * 60 * 60));
|
||||
assert_duration_eq(days_to_duration(1.5), Duration::from_secs(36 * 60 * 60));
|
||||
assert_duration_eq(days_to_duration(0.25), Duration::from_secs(6 * 60 * 60));
|
||||
assert_duration_eq(
|
||||
days_to_duration(0.1),
|
||||
Duration::from_secs_f64(0.1 * 24.0 * 60.0 * 60.0),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_days_to_duration_large_number() {
|
||||
let large_days = 1000.0;
|
||||
let expected_seconds = large_days * 24.0 * 60.0 * 60.0;
|
||||
assert_duration_eq(
|
||||
days_to_duration(large_days),
|
||||
Duration::from_secs_f64(expected_seconds),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_days_to_duration_very_small() {
|
||||
let small_days = 1e-6;
|
||||
let expected_seconds = small_days * 24.0 * 60.0 * 60.0;
|
||||
assert_duration_eq(
|
||||
days_to_duration(small_days),
|
||||
Duration::from_secs_f64(expected_seconds),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
use mcps::{
|
||||
schedule::Schedule,
|
||||
simulation::run_multiple_simulations,
|
||||
task::{days_to_duration, Task},
|
||||
};
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
#[test]
|
||||
fn test_average_effort_calculation() {
|
||||
let num_tasks = 10;
|
||||
let min_days = 5.0;
|
||||
let max_days = 15.0;
|
||||
let expected_avg_days = 10.0;
|
||||
let estimate_confidence = 0.8;
|
||||
let num_simulations = 10000;
|
||||
let num_workers = 1; // Using 1 worker to simplify effort calculation
|
||||
|
||||
// Create tasks
|
||||
let tasks: Vec<Task> = (0..num_tasks)
|
||||
.map(|i| {
|
||||
Task::new(
|
||||
&format!("Task{}", i),
|
||||
vec![],
|
||||
days_to_duration(min_days),
|
||||
days_to_duration(max_days),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let schedule =
|
||||
Schedule::new(tasks, num_workers, estimate_confidence).expect("Failed to create schedule");
|
||||
|
||||
let (_, effort_times) = run_multiple_simulations(&schedule, num_simulations);
|
||||
|
||||
let total_effort: Duration = effort_times.iter().sum();
|
||||
let avg_effort = total_effort / num_simulations as u32;
|
||||
let avg_effort_days = avg_effort.as_secs_f64() / 86400.0;
|
||||
|
||||
println!(
|
||||
"Average effort: {:.2} days (expected: {:.2} days)",
|
||||
avg_effort_days,
|
||||
expected_avg_days * num_tasks as f64
|
||||
);
|
||||
|
||||
// Check if the average effort is within 1% of the expected value
|
||||
let expected_total_days = expected_avg_days * num_tasks as f64;
|
||||
let difference_percentage =
|
||||
(avg_effort_days - expected_total_days).abs() / expected_total_days * 100.0;
|
||||
|
||||
assert!(
|
||||
difference_percentage < 1.0,
|
||||
"Average effort ({:.2} days) deviates more than 1% from expected ({:.2} days)",
|
||||
avg_effort_days,
|
||||
expected_total_days
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
use mcps::{
|
||||
schedule::Schedule,
|
||||
task::{days_to_duration, Task},
|
||||
};
|
||||
|
||||
fn create_schedule_and_expect_error(tasks: Vec<Task>, expected_error: &str) {
|
||||
let result = Schedule::new(tasks, 1, 0.8);
|
||||
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"Expected schedule creation to fail, but it didn't"
|
||||
);
|
||||
if let Err(error_message) = result {
|
||||
assert!(
|
||||
error_message.contains(expected_error),
|
||||
"Expected error message '{}', but got '{}'",
|
||||
expected_error,
|
||||
error_message
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cyclic_dependency() {
|
||||
let tasks = vec![
|
||||
Task::new(
|
||||
"A",
|
||||
vec!["B".to_string()],
|
||||
days_to_duration(1.0),
|
||||
days_to_duration(2.0),
|
||||
),
|
||||
Task::new(
|
||||
"B",
|
||||
vec!["A".to_string()],
|
||||
days_to_duration(1.0),
|
||||
days_to_duration(2.0),
|
||||
),
|
||||
];
|
||||
|
||||
create_schedule_and_expect_error(tasks, "Cyclic dependency detected");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_missing_dependency() {
|
||||
let tasks = vec![
|
||||
Task::new(
|
||||
"A",
|
||||
vec!["C".to_string()],
|
||||
days_to_duration(1.0),
|
||||
days_to_duration(2.0),
|
||||
),
|
||||
Task::new(
|
||||
"B",
|
||||
vec!["A".to_string()],
|
||||
days_to_duration(1.0),
|
||||
days_to_duration(2.0),
|
||||
),
|
||||
];
|
||||
|
||||
create_schedule_and_expect_error(tasks, "Missing dependency");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_negative_duration() {
|
||||
let tasks = vec![Task::new(
|
||||
"A",
|
||||
vec![],
|
||||
days_to_duration(0.0),
|
||||
days_to_duration(1.0),
|
||||
)];
|
||||
|
||||
create_schedule_and_expect_error(tasks, "Invalid task duration");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_min_greater_than_max() {
|
||||
let tasks = vec![Task::new(
|
||||
"A",
|
||||
vec![],
|
||||
days_to_duration(3.0),
|
||||
days_to_duration(2.0),
|
||||
)];
|
||||
|
||||
create_schedule_and_expect_error(tasks, "Minimum duration greater than maximum");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_task_list() {
|
||||
let tasks = vec![];
|
||||
create_schedule_and_expect_error(tasks, "Empty task list");
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
use mcps::{
|
||||
schedule::Schedule,
|
||||
simulation::run_multiple_simulations,
|
||||
task::{days_to_duration, Task},
|
||||
};
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
fn create_independent_tasks(num_tasks: usize) -> Vec<Task> {
|
||||
(0..num_tasks)
|
||||
.map(|i| {
|
||||
Task::new(
|
||||
&format!("Task_{}", i),
|
||||
vec![],
|
||||
days_to_duration(1.0),
|
||||
days_to_duration(3.0),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_dependencies_schedule() {
|
||||
let num_tasks = 32;
|
||||
let tasks = create_independent_tasks(num_tasks);
|
||||
let estimate_confidence = 0.8;
|
||||
let num_simulations = 1_000;
|
||||
|
||||
let mut prev_avg_effort = 0.0;
|
||||
|
||||
for num_workers in [1, 2, 4, 8] {
|
||||
let schedule = Schedule::new(tasks.clone(), num_workers, estimate_confidence)
|
||||
.expect("Failed to create schedule");
|
||||
let (project_durations, effort_times) =
|
||||
run_multiple_simulations(&schedule, num_simulations);
|
||||
|
||||
let avg_duration = project_durations.iter().sum::<Duration>() / num_simulations as u32;
|
||||
let avg_effort = effort_times.iter().sum::<Duration>() / num_simulations as u32;
|
||||
|
||||
let avg_duration_days = avg_duration.as_secs_f64() / 86400.0;
|
||||
let avg_effort_days = avg_effort.as_secs_f64() / 86400.0;
|
||||
|
||||
println!(
|
||||
"Workers: {}, Avg Duration: {:.2} days, Avg Effort: {:.2} person-days",
|
||||
num_workers, avg_duration_days, avg_effort_days
|
||||
);
|
||||
|
||||
// Check that effort is consistent across different numbers of workers
|
||||
if num_workers > 1 {
|
||||
let effort_difference = (avg_effort_days - prev_avg_effort).abs();
|
||||
assert!(
|
||||
effort_difference < 2.0,
|
||||
"Effort should be consistent. Previous: {:.2}, Current: {:.2}",
|
||||
prev_avg_effort,
|
||||
avg_effort_days
|
||||
);
|
||||
}
|
||||
prev_avg_effort = avg_effort_days;
|
||||
|
||||
// Check effort/duration ratio
|
||||
let effort_ratio = avg_effort_days / avg_duration_days;
|
||||
let expected_ratio = num_workers as f64;
|
||||
let ratio_error = (effort_ratio - expected_ratio).abs() / expected_ratio;
|
||||
|
||||
println!(
|
||||
" Effort/Duration Ratio: {:.2} (Expected: {:.2}, Error: {:.2})",
|
||||
effort_ratio, expected_ratio, ratio_error
|
||||
);
|
||||
|
||||
assert!(ratio_error < 0.20, "Expected effort/duration ratio to be close to number of workers. Expected: {}, Actual: {}", expected_ratio, effort_ratio);
|
||||
|
||||
// For 1 worker, duration should be very close to effort
|
||||
if num_workers == 1 {
|
||||
let duration_effort_ratio = avg_duration_days / avg_effort_days;
|
||||
assert!(
|
||||
(duration_effort_ratio - 1.0).abs() < 0.05,
|
||||
"For single worker, duration should equal effort. Ratio: {}",
|
||||
duration_effort_ratio
|
||||
);
|
||||
}
|
||||
|
||||
// Check that duration decreases proportionally to the increase in workers
|
||||
if num_workers > 1 {
|
||||
let expected_duration = avg_effort_days / num_workers as f64;
|
||||
let actual_duration = avg_duration_days;
|
||||
let duration_ratio = actual_duration / expected_duration;
|
||||
let duration_error = (duration_ratio - 1.0).abs();
|
||||
|
||||
println!(
|
||||
" Duration Ratio: {:.2} (Expected: {:.2}, Actual: {:.2}, Error: {:.2})",
|
||||
duration_ratio, expected_duration, actual_duration, duration_error
|
||||
);
|
||||
|
||||
assert!(duration_error < 0.20, "Expected duration to decrease proportionally with workers. Expected: {:.2}, Actual: {:.2}", expected_duration, actual_duration);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
use mcps::{
|
||||
schedule::Schedule,
|
||||
simulation::run_multiple_simulations,
|
||||
task::{days_to_duration, Task},
|
||||
};
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
#[test]
|
||||
fn test_simulation_with_different_worker_counts() {
|
||||
let tasks = vec![
|
||||
Task::new("A", vec![], days_to_duration(1.0), days_to_duration(3.0)),
|
||||
Task::new(
|
||||
"B",
|
||||
vec!["A".to_string()],
|
||||
days_to_duration(2.0),
|
||||
days_to_duration(4.0),
|
||||
),
|
||||
Task::new(
|
||||
"C",
|
||||
vec!["A".to_string()],
|
||||
days_to_duration(3.0),
|
||||
days_to_duration(5.0),
|
||||
),
|
||||
Task::new(
|
||||
"D",
|
||||
vec!["B".to_string(), "C".to_string()],
|
||||
days_to_duration(2.0),
|
||||
days_to_duration(6.0),
|
||||
),
|
||||
Task::new(
|
||||
"E",
|
||||
vec!["B".to_string()],
|
||||
days_to_duration(1.0),
|
||||
days_to_duration(2.0),
|
||||
),
|
||||
Task::new(
|
||||
"F",
|
||||
vec!["D".to_string(), "E".to_string()],
|
||||
days_to_duration(2.0),
|
||||
days_to_duration(4.0),
|
||||
),
|
||||
];
|
||||
|
||||
let estimate_confidence = 0.8;
|
||||
let num_simulations = 1000;
|
||||
|
||||
let mut all_efforts = Vec::new();
|
||||
|
||||
for num_workers in [1, 2, 4, 8, 16, 32] {
|
||||
let schedule = Schedule::new(tasks.clone(), num_workers, estimate_confidence)
|
||||
.expect("Failed to create schedule");
|
||||
let (project_durations, effort_times) =
|
||||
run_multiple_simulations(&schedule, num_simulations);
|
||||
|
||||
let avg_duration = project_durations.iter().sum::<Duration>() / num_simulations as u32;
|
||||
let avg_effort = effort_times.iter().sum::<Duration>() / num_simulations as u32;
|
||||
|
||||
println!(
|
||||
"Workers: {}, Avg Duration: {:.2} days, Avg Effort: {:.2} person-days",
|
||||
num_workers,
|
||||
avg_duration.as_secs_f64() / 86400.0,
|
||||
avg_effort.as_secs_f64() / 86400.0
|
||||
);
|
||||
|
||||
all_efforts.push(avg_effort);
|
||||
|
||||
// Basic sanity checks
|
||||
assert!(
|
||||
avg_duration <= avg_effort,
|
||||
"Average duration should not exceed average effort"
|
||||
);
|
||||
if num_workers > 1 {
|
||||
assert!(
|
||||
avg_duration < avg_effort,
|
||||
"With multiple workers, average duration should be less than average effort"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Check that effort remains relatively constant regardless of worker count
|
||||
let max_effort = all_efforts.iter().max().unwrap();
|
||||
let min_effort = all_efforts.iter().min().unwrap();
|
||||
let effort_difference = (max_effort.as_secs_f64() - min_effort.as_secs_f64()) / 86400.0;
|
||||
|
||||
println!("Maximum effort difference: {:.2} days", effort_difference);
|
||||
assert!(
|
||||
effort_difference < 1.0,
|
||||
"Average effort should not vary significantly with different worker counts"
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user