diff --git a/Cargo.lock b/Cargo.lock index fde62a89..5515850b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -43,23 +43,24 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.4" +version = "0.6.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" +checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", + "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" -version = "1.0.4" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" +checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" [[package]] name = "anstyle-parse" @@ -116,7 +117,7 @@ checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.66", ] [[package]] @@ -238,7 +239,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.66", ] [[package]] @@ -255,12 +256,16 @@ dependencies = [ "async-compression", "base64", "clap", + "console", "git2", + "gql_client", + "indicatif", "insta", "itertools", "lazy_static", "log", "md5", + "nestify", "rand", "regex", "reqwest", @@ -268,8 +273,10 @@ dependencies = [ "reqwest-retry", "serde", "serde_json", + "serde_yaml", "sha256", "simplelog", + "sysinfo", "temp-env", "tempfile", "tokio", @@ -285,14 +292,15 @@ checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" [[package]] name = "console" -version = "0.15.7" +version = "0.15.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" dependencies = [ "encode_unicode", "lazy_static", "libc", - "windows-sys 0.45.0", + "unicode-width", + "windows-sys 0.52.0", ] [[package]] @@ -329,6 +337,31 @@ dependencies = [ "cfg-if", ] +[[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 = "crypto-common" version = "0.1.6" @@ -509,7 +542,7 @@ checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.66", ] [[package]] @@ -586,6 +619,17 @@ dependencies = [ "url", ] +[[package]] +name = "gql_client" +version = "1.0.7" +source = "git+https://github.com/CodSpeedHQ/gql-client-rs#83610cc89083cf3b18d7b7e539e76e82121b6ebb" +dependencies = [ + "log", + "reqwest", + "serde", + "serde_json", +] + [[package]] name = "h2" version = "0.3.21" @@ -711,7 +755,7 @@ dependencies = [ "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "windows-core", + "windows-core 0.51.1", ] [[package]] @@ -745,14 +789,27 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.1.0" +version = "2.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", "hashbrown 0.14.3", ] +[[package]] +name = "indicatif" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "763a5a8f45087d6bcea4222e7b72c291a054edf80e4ef6efd2a4979878c7bea3" +dependencies = [ + "console", + "instant", + "number_prefix", + "portable-atomic", + "unicode-width", +] + [[package]] name = "insta" version = "1.34.0" @@ -787,6 +844,12 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +[[package]] +name = "is_terminal_polyfill" +version = "1.70.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" + [[package]] name = "itertools" version = "0.11.0" @@ -798,9 +861,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.9" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "jobserver" @@ -896,9 +959,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.20" +version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "md5" @@ -908,9 +971,9 @@ checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" [[package]] name = "memchr" -version = "2.6.4" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" [[package]] name = "mime" @@ -966,6 +1029,27 @@ dependencies = [ "tempfile", ] +[[package]] +name = "nestify" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d7249f7122d4e8a40f3b1b1850b763d2f864bf8e4b712427f024f8a167ea17" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.66", +] + +[[package]] +name = "ntapi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" +dependencies = [ + "winapi", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -981,6 +1065,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + [[package]] name = "object" version = "0.32.1" @@ -1019,7 +1109,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.66", ] [[package]] @@ -1135,7 +1225,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn", + "syn 2.0.66", ] [[package]] @@ -1167,6 +1257,12 @@ version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +[[package]] +name = "portable-atomic" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" + [[package]] name = "powerfmt" version = "0.2.0" @@ -1179,20 +1275,44 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro2" -version = "1.0.69" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +checksum = "0b33eb56c327dec362a9e55b3ad14f9d2f0904fb5a5b03b513ab5465399e9f43" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.33" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] @@ -1227,6 +1347,26 @@ dependencies = [ "getrandom", ] +[[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 = "redox_syscall" version = "0.2.16" @@ -1394,9 +1534,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.15" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "schannel" @@ -1438,31 +1578,31 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.192" +version = "1.0.202" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001" +checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.192" +version = "1.0.202" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1" +checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.66", ] [[package]] name = "serde_json" -version = "1.0.108" +version = "1.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" +checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" dependencies = [ - "indexmap 2.1.0", + "indexmap 2.2.6", "itoa", "ryu", "serde", @@ -1480,6 +1620,19 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap 2.2.6", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "sha2" version = "0.10.8" @@ -1517,6 +1670,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acee08041c5de3d5048c8b3f6f13fafb3026b24ba43c6a695a0c76179b844369" dependencies = [ "log", + "termcolor", "time", ] @@ -1563,15 +1717,41 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" -version = "2.0.39" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "sysinfo" +version = "0.30.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "732ffa00f53e6b2af46208fba5718d9662a421049204e156328b66791ffa15ae" +dependencies = [ + "cfg-if", + "core-foundation-sys", + "libc", + "ntapi", + "once_cell", + "rayon", + "serde", + "windows", +] + [[package]] name = "system-configuration" version = "0.5.1" @@ -1624,6 +1804,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + [[package]] name = "thiserror" version = "1.0.50" @@ -1641,7 +1830,7 @@ checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.66", ] [[package]] @@ -1714,7 +1903,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.66", ] [[package]] @@ -1792,7 +1981,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.66", ] [[package]] @@ -1852,6 +2041,18 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-width" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + [[package]] name = "url" version = "2.4.1" @@ -1917,7 +2118,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn", + "syn 2.0.66", "wasm-bindgen-shared", ] @@ -1951,7 +2152,7 @@ checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.66", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -2016,12 +2217,31 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +dependencies = [ + "windows-core 0.52.0", + "windows-targets 0.52.0", +] + [[package]] name = "windows-core" version = "0.51.1" @@ -2032,12 +2252,12 @@ dependencies = [ ] [[package]] -name = "windows-sys" -version = "0.45.0" +name = "windows-core" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.42.2", + "windows-targets 0.52.0", ] [[package]] @@ -2058,21 +2278,6 @@ dependencies = [ "windows-targets 0.52.0", ] -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - [[package]] name = "windows-targets" version = "0.48.5" @@ -2103,12 +2308,6 @@ dependencies = [ "windows_x86_64_msvc 0.52.0", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -2121,12 +2320,6 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -2139,12 +2332,6 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -2157,12 +2344,6 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -2175,12 +2356,6 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -2193,12 +2368,6 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -2211,12 +2380,6 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - [[package]] name = "windows_x86_64_msvc" version = "0.48.5" diff --git a/Cargo.toml b/Cargo.toml index 51d5b94c..fba7cf7f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,10 +5,14 @@ edition = "2021" repository = "https://github.com/CodSpeedHQ/runner" publish = false +[[bin]] +name = "codspeed" +path = "src/main.rs" + [dependencies] anyhow = "1.0.75" -clap = { version = "4.4.8", features = ["derive", "env"] } +clap = { version = "4.4.8", features = ["derive", "env", "color"] } itertools = "0.11.0" lazy_static = "1.4.0" log = "0.4.20" @@ -30,9 +34,17 @@ tokio-tar = "0.3.1" md5 = "0.7.0" base64 = "0.21.0" async-compression = { version = "0.4.5", features = ["tokio", "gzip"] } -simplelog = { version = "0.12.1", default-features = false } +simplelog = { version = "0.12.1", default-features = false, features = [ + "termcolor", +] } tempfile = "3.10.0" git2 = "0.18.3" +nestify = "0.3.3" +gql_client = { git = "https://github.com/CodSpeedHQ/gql-client-rs" } +serde_yaml = "0.9.34" +sysinfo = { version = "0.30.12", features = ["serde"] } +indicatif = "0.17.8" +console = "0.15.8" [dev-dependencies] temp-env = { version = "0.3.6", features = ["async_closure"] } diff --git a/README.md b/README.md index 976b9864..f8afcb16 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@
-

codspeed-runner

+

CodSpeed CLI

-CLI to gather performance data from CI environments and upload performance reports to [CodSpeed](https://codspeed.io) +CLI to gather performance data and upload performance reports to [CodSpeed](https://codspeed.io) [![CI](https://github.com/CodSpeedHQ/runner/actions/workflows/ci.yml/badge.svg)](https://github.com/CodSpeedHQ/runner/actions/workflows/ci.yml) [![Discord](https://img.shields.io/badge/chat%20on-discord-7289da.svg)](https://discord.com/invite/MxpaCfKSqF) @@ -9,9 +9,9 @@ CLI to gather performance data from CI environments and upload performance repor
-The `codspeed-runner` CLI is designed to be used in CI environments. +The `codspeed` CLI is designed to be used both in **local** in **CI environments**. -The following providers are supported: +The following CI providers are supported: - [GitHub Actions](https://docs.codspeed.io/ci/github-actions): Usage with [`@CodSpeedHQ/action`](https://github.com/CodSpeedHQ/action) is recommended. - [Buildkite](https://docs.codspeed.io/ci/buildkite) @@ -20,7 +20,7 @@ The following providers are supported: If you want to use the CLI with another provider, you can open an issue or chat with us on [Discord](https://discord.com/invite/MxpaCfKSqF) 🚀 -You can check out the implementation of the [supported providers](https://github.com/CodSpeedHQ/runner/tree/main/src/ci_provider) for reference. +You can check out the implementation of the [supported providers](https://github.com/CodSpeedHQ/runner/tree/main/src/run/ci_provider) for reference. ## Installation @@ -35,16 +35,31 @@ Refer to the [releases page](https://github.com/CodSpeedHQ/runner/releases) to s ## Usage > [!NOTE] -> For now, the CLI only supports Ubuntu 20.04 and 22.04. +> For now, the CLI only supports Ubuntu 20.04, 22.04, and Debian 11, 12. -Example of a command to run benchmarks with [Vitest](https://docs.codspeed.io/benchmarks/nodejs/vitest): +First, authenticate with your CodSpeed account: ```bash -codspeed-runner --token=$CODSPEED_TOKEN -- pnpm vitest bench +codspeed auth login +``` + +Then, run benchmarks with the following command: + +```bash +codspeed run + +# Example, using https://github.com/CodSpeedHQ/codspeed-rust +codspeed run cargo codspeed run + +# Example, using https://github.com/CodSpeedHQ/pytest-codspeed +codspeed run pytest ./tests --codspeed + +# Example, using https://github.com/CodSpeedHQ/codspeed-node/tree/main/packages/vitest-plugin +codspeed run pnpm vitest bench ``` ``` -Usage: codspeed-runner [OPTIONS] [COMMAND]... +Usage: codspeed run [OPTIONS] [COMMAND]... Arguments: [COMMAND]... The bench command to run @@ -65,5 +80,5 @@ Options: Use the `CODSPEED_LOG` environment variable to set the logging level: ```bash -CODSPEED_LOG=debug codspeed-runner ... +CODSPEED_LOG=debug codspeed run ... ``` diff --git a/src/api_client.rs b/src/api_client.rs new file mode 100644 index 00000000..40a2e256 --- /dev/null +++ b/src/api_client.rs @@ -0,0 +1,216 @@ +use std::fmt::Display; + +use crate::prelude::*; +use crate::{app::Cli, config::CodSpeedConfig}; +use console::style; +use gql_client::{Client as GQLClient, ClientConfig}; +use nestify::nest; +use serde::{Deserialize, Serialize}; + +pub struct CodSpeedAPIClient { + gql_client: GQLClient, + unauthenticated_gql_client: GQLClient, +} + +impl TryFrom<&Cli> for CodSpeedAPIClient { + type Error = Error; + fn try_from(args: &Cli) -> Result { + let codspeed_config = CodSpeedConfig::load()?; + + Ok(Self { + gql_client: build_gql_api_client(&codspeed_config, args.api_url.clone(), true), + unauthenticated_gql_client: build_gql_api_client( + &codspeed_config, + args.api_url.clone(), + false, + ), + }) + } +} + +fn build_gql_api_client( + codspeed_config: &CodSpeedConfig, + api_url: String, + with_auth: bool, +) -> GQLClient { + let headers = if with_auth && codspeed_config.auth.token.is_some() { + let mut headers = std::collections::HashMap::new(); + headers.insert( + "Authorization".to_string(), + codspeed_config.auth.token.clone().unwrap(), + ); + headers + } else { + Default::default() + }; + + GQLClient::new_with_config(ClientConfig { + endpoint: api_url, + timeout: Some(10), + headers: Some(headers), + proxy: None, + }) +} + +nest! { + #[derive(Debug, Deserialize, Serialize)]* + #[serde(rename_all = "camelCase")]* + struct CreateLoginSessionData { + create_login_session: pub struct CreateLoginSessionPayload { + pub callback_url: String, + pub session_id: String, + } + } +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct ConsumeLoginSessionVars { + session_id: String, +} +nest! { + #[derive(Debug, Deserialize, Serialize)]* + #[serde(rename_all = "camelCase")]* + struct ConsumeLoginSessionData { + consume_login_session: pub struct ConsumeLoginSessionPayload { + pub token: Option + } + } +} + +#[derive(Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct FetchLocalRunReportVars { + pub owner: String, + pub name: String, + pub run_id: String, +} +#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)] +pub enum ReportConclusion { + AcknowledgedFailure, + Failure, + MissingBaseRun, + Success, +} + +impl Display for ReportConclusion { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ReportConclusion::AcknowledgedFailure => { + write!(f, "{}", style("Acknowledged Failure").yellow().bold()) + } + ReportConclusion::Failure => write!(f, "{}", style("Failure").red().bold()), + ReportConclusion::MissingBaseRun => { + write!(f, "{}", style("Missing Base Run").yellow().bold()) + } + ReportConclusion::Success => write!(f, "{}", style("Success").green().bold()), + } + } +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct FetchLocalRunReportHeadReport { + pub id: String, + pub impact: Option, + pub conclusion: ReportConclusion, +} +#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum RunStatus { + Pending, + Processing, + Completed, +} +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct FetchLocalRunReportRun { + pub id: String, + pub status: RunStatus, + pub url: String, + pub head_reports: Vec, +} +nest! { + #[derive(Debug, Deserialize, Serialize)]* + #[serde(rename_all = "camelCase")]* + struct FetchLocalRunReportData { + repository: pub struct FetchLocalRunReportRepository { + settings: struct FetchLocalRunReportSettings { + allowed_regression: f64, + }, + runs: Vec, + } + } +} + +pub struct FetchLocalRunReportResponse { + pub allowed_regression: f64, + pub run: FetchLocalRunReportRun, +} + +impl CodSpeedAPIClient { + pub async fn create_login_session(&self) -> Result { + let response = self + .unauthenticated_gql_client + .query_unwrap::(include_str!("queries/CreateLoginSession.gql")) + .await; + match response { + Ok(response) => Ok(response.create_login_session), + Err(err) => bail!("Failed to create login session: {}", err), + } + } + + pub async fn consume_login_session( + &self, + session_id: &str, + ) -> Result { + let response = self + .unauthenticated_gql_client + .query_with_vars_unwrap::( + include_str!("queries/ConsumeLoginSession.gql"), + ConsumeLoginSessionVars { + session_id: session_id.to_string(), + }, + ) + .await; + match response { + Ok(response) => Ok(response.consume_login_session), + Err(err) => bail!("Failed to use login session: {}", err), + } + } + + pub async fn fetch_local_run_report( + &self, + vars: FetchLocalRunReportVars, + ) -> Result { + let response = self + .gql_client + .query_with_vars_unwrap::( + include_str!("queries/FetchLocalRunReport.gql"), + vars.clone(), + ) + .await; + match response { + Ok(response) => { + let allowed_regression = response.repository.settings.allowed_regression; + + match response.repository.runs.into_iter().next() { + Some(run) => Ok(FetchLocalRunReportResponse { + allowed_regression, + run, + }), + None => bail!( + "No runs found for owner: {}, name: {}, run_id: {}", + vars.owner, + vars.name, + vars.run_id + ), + } + } + Err(err) if err.contains_error_code("UNAUTHENTICATED") => { + bail!("Your session has expired, please login again using `codspeed auth login`") + } + Err(err) => bail!("Failed to fetch local run report: {}", err), + } + } +} diff --git a/src/app.rs b/src/app.rs index 29ca3b11..47d8ebf0 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,97 +1,53 @@ -use crate::{ci_provider, config::Config, logger::Logger, prelude::*, runner, uploader, VERSION}; -use clap::Parser; - -fn show_banner() { - let banner = format!( - r#" - ______ __ _____ __ - / ____/____ ____/ // ___/ ____ ___ ___ ____/ / - / / / __ \ / __ / \__ \ / __ \ / _ \ / _ \ / __ / -/ /___ / /_/ // /_/ / ___/ // /_/ // __// __// /_/ / -\____/ \____/ \__,_/ /____// .___/ \___/ \___/ \__,_/ - https://codspeed.io /_/ runner v{} -"#, - VERSION - ); - println!("{}", banner); - debug!("codspeed-runner v{}", VERSION); +use crate::{ + api_client::CodSpeedAPIClient, auth, local_logger::CODSPEED_U8_COLOR_CODE, prelude::*, run, +}; +use clap::{ + builder::{styling, Styles}, + Parser, Subcommand, +}; + +fn create_styles() -> Styles { + styling::Styles::styled() + .header(styling::AnsiColor::Green.on_default() | styling::Effects::BOLD) + .usage(styling::AnsiColor::Green.on_default() | styling::Effects::BOLD) + .literal( + styling::Ansi256Color(CODSPEED_U8_COLOR_CODE).on_default() | styling::Effects::BOLD, + ) + .placeholder(styling::AnsiColor::Cyan.on_default()) } #[derive(Parser, Debug)] -pub struct AppArgs { - /// The upload URL to use for uploading the results, useful for on-premises installations - #[arg(long)] - pub upload_url: Option, - - /// The token to use for uploading the results, - #[arg(long, env = "CODSPEED_TOKEN")] - pub token: Option, - - /// The directory where the command will be executed. - #[arg(long)] - pub working_directory: Option, - - /// Comma-separated list of instruments to enable. Possible values: mongodb. - #[arg(long, value_delimiter = ',')] - pub instruments: Vec, - - /// The name of the environment variable that contains the MongoDB URI to patch. - /// If not provided, user will have to provide it dynamically through a CodSpeed integration. - /// - /// Only used if the `mongodb` instrument is enabled. - #[arg(long)] - pub mongo_uri_env_name: Option, - - /// Only for debugging purposes, skips the upload of the results +#[command(about = "The CodSpeed CLI tool", styles = create_styles())] +pub struct Cli { + /// The URL of the CodSpeed GraphQL API #[arg( long, - default_value = "false", + env = "CODSPEED_API_URL", + global = true, hide = true, - env = "CODSPEED_SKIP_UPLOAD" + default_value = "https://gql.codspeed.io/" )] - pub skip_upload: bool, - - /// Only for debugging purposes, skips the setup of the runner - #[arg(long, default_value = "false", hide = true)] - pub skip_setup: bool, + pub api_url: String, - /// The bench command to run - pub command: Vec, + #[command(subcommand)] + command: Commands, } -#[cfg(test)] -impl AppArgs { - /// Constructs a new `AppArgs` with default values for testing purposes - pub fn test() -> Self { - Self { - upload_url: None, - token: None, - working_directory: None, - instruments: vec![], - mongo_uri_env_name: None, - skip_upload: false, - skip_setup: false, - command: vec![], - } - } +#[derive(Subcommand, Debug)] +enum Commands { + /// Run the bench command and upload the results to CodSpeed + Run(run::RunArgs), + /// Commands related to authentication with CodSpeed + Auth(auth::AuthArgs), } pub async fn run() -> Result<()> { - let args = AppArgs::parse(); - let config = Config::try_from(args)?; - let provider = ci_provider::get_provider(&config)?; - let logger = Logger::new(&provider)?; - - show_banner(); - debug!("config: {:#?}", config); - - let run_data = runner::run(&config).await?; + let cli = Cli::parse(); + let api_client = CodSpeedAPIClient::try_from(&cli)?; - if !config.skip_upload { - start_group!("Upload the results"); - logger.persist_log_to_profile_folder(&run_data)?; - uploader::upload(&config, provider, &run_data).await?; - end_group!(); + match cli.command { + Commands::Run(args) => run::run(args, &api_client).await?, + Commands::Auth(args) => auth::run(args, &api_client).await?, } Ok(()) } diff --git a/src/auth.rs b/src/auth.rs new file mode 100644 index 00000000..23f27fc4 --- /dev/null +++ b/src/auth.rs @@ -0,0 +1,83 @@ +use std::time::Duration; + +use crate::local_logger::get_local_logger; +use crate::{api_client::CodSpeedAPIClient, config::CodSpeedConfig, prelude::*}; +use clap::{Args, Subcommand}; +use console::style; +use simplelog::CombinedLogger; +use tokio::time::{sleep, Instant}; + +#[derive(Debug, Args)] +pub struct AuthArgs { + #[command(subcommand)] + command: AuthCommands, +} + +#[derive(Debug, Subcommand)] +enum AuthCommands { + /// Login to CodSpeed + Login, +} + +fn init_logger() -> Result<()> { + let logger = get_local_logger(); + CombinedLogger::init(vec![logger])?; + Ok(()) +} + +pub async fn run(args: AuthArgs, api_client: &CodSpeedAPIClient) -> Result<()> { + init_logger()?; + + match args.command { + AuthCommands::Login => login(api_client).await?, + } + Ok(()) +} + +const LOGIN_SESSION_MAX_DURATION: Duration = Duration::from_secs(60 * 5); // 5 minutes + +async fn login(api_client: &CodSpeedAPIClient) -> Result<()> { + debug!("Login to CodSpeed"); + start_group!("Creating login session"); + let login_session_payload = api_client.create_login_session().await?; + end_group!(); + + info!( + "Login session created, open the following URL in your browser: {}\n", + style(login_session_payload.callback_url) + .blue() + .bold() + .underlined() + ); + + start_group!("Waiting for the login to be completed"); + let token; + let start = Instant::now(); + loop { + if start.elapsed() > LOGIN_SESSION_MAX_DURATION { + bail!("Login session expired, please try again"); + } + + match api_client + .consume_login_session(&login_session_payload.session_id) + .await? + .token + { + Some(token_from_api) => { + token = token_from_api; + break; + } + None => sleep(Duration::from_secs(5)).await, + } + } + end_group!(); + + let mut config = CodSpeedConfig::load()?; + config.auth.token = Some(token); + config.persist()?; + debug!("Token saved to configuration file"); + + info!("Login successful, your are now authenticated on CodSpeed"); + + Ok(()) +} diff --git a/src/ci_provider/logger.rs b/src/ci_provider/logger.rs deleted file mode 100644 index ebd61933..00000000 --- a/src/ci_provider/logger.rs +++ /dev/null @@ -1,80 +0,0 @@ -use crate::runner::VALGRIND_EXECUTION_TARGET; - -/// This target is used exclusively to handle group events. -pub const GROUP_TARGET: &str = "codspeed::group"; -pub const OPENED_GROUP_TARGET: &str = "codspeed::group::opened"; - -#[macro_export] -/// Start a new log group. All logs between this and the next `end_group!` will be grouped together. -/// -/// # Example -/// -/// ```rust -/// start_group!("My group"); -/// info!("This will be grouped"); -/// end_group!(); -/// ``` -macro_rules! start_group { - ($name:expr) => { - log::log!(target: $crate::ci_provider::logger::GROUP_TARGET, log::Level::Info, "{}", $name); - }; -} - -#[macro_export] -/// Start a new opened log group. All logs between this and the next `end_group!` will be grouped together. -/// -/// # Example -/// -/// ```rust -/// start_opened_group!("My group"); -/// info!("This will be grouped"); -/// end_group!(); -/// ``` -macro_rules! start_opened_group { - ($name:expr) => { - log::log!(target: $crate::ci_provider::logger::OPENED_GROUP_TARGET, log::Level::Info, "{}", $name); - }; -} - -#[macro_export] -/// End the current log group. -/// See [`start_group!`] for more information. -macro_rules! end_group { - () => { - log::log!(target: $crate::ci_provider::logger::GROUP_TARGET, log::Level::Info, ""); - }; -} - -pub enum GroupEvent { - Start(String), - StartOpened(String), - End, -} - -/// Returns the group event if the record is a group event, otherwise returns `None`. -pub(super) fn get_group_event(record: &log::Record) -> Option { - match record.target() { - OPENED_GROUP_TARGET => { - let args = record.args().to_string(); - if args.is_empty() { - None - } else { - Some(GroupEvent::StartOpened(args)) - } - } - GROUP_TARGET => { - let args = record.args().to_string(); - if args.is_empty() { - Some(GroupEvent::End) - } else { - Some(GroupEvent::Start(args)) - } - } - _ => None, - } -} - -pub(super) fn should_provider_logger_handle_record(record: &log::Record) -> bool { - // Provider logger should handle all records except the ones from the valgrind execution target - record.target() != VALGRIND_EXECUTION_TARGET -} diff --git a/src/ci_provider/snapshots/codspeed_runner__ci_provider__github_actions_provider__tests__pull_request_upload_metadata.snap b/src/ci_provider/snapshots/codspeed_runner__ci_provider__github_actions_provider__tests__pull_request_upload_metadata.snap deleted file mode 100644 index 6dc82146..00000000 --- a/src/ci_provider/snapshots/codspeed_runner__ci_provider__github_actions_provider__tests__pull_request_upload_metadata.snap +++ /dev/null @@ -1,28 +0,0 @@ ---- -source: src/ci_provider/github_actions_provider.rs -expression: upload_metadata ---- -{ - "version": 1, - "tokenless": false, - "ref": "refs/pull/22/merge", - "headRef": "feat/codspeed-runner", - "baseRef": "main", - "owner": "my-org", - "repository": "adrien-python-test", - "commitHash": "24809d9fca9ad0808a777bcbd807ecd5ec8a9100", - "event": "pull_request", - "profileMd5": "archive_hash", - "ghData": { - "runId": 6957110437, - "job": "log-env", - "sender": { - "id": 19605940, - "login": "adriencaccia" - } - }, - "runner": { - "name": "codspeed-runner", - "version": "[version]" - } -} diff --git a/src/config.rs b/src/config.rs index 99790712..3fc6435c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,116 +1,73 @@ -use crate::{instruments::Instruments, prelude::*}; -use url::Url; +use std::{env, fs, path::PathBuf}; -use crate::app::AppArgs; +use crate::prelude::*; +use nestify::nest; +use serde::{Deserialize, Serialize}; -#[derive(Debug)] -pub struct Config { - pub upload_url: Url, - pub token: Option, - pub working_directory: Option, - pub command: String, - - pub instruments: Instruments, +nest! { + #[derive(Debug, Deserialize, Serialize)]* + #[serde(rename_all = "kebab-case")]* + pub struct CodSpeedConfig { + pub auth: pub struct AuthConfig { + pub token: Option, + } + } +} - pub skip_upload: bool, - pub skip_setup: bool, +/// Get the path to the configuration file, following the XDG Base Directory Specification +/// at https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html +fn get_configuration_file_path() -> PathBuf { + let config_dir = env::var("XDG_CONFIG_HOME") + .map(PathBuf::from) + .unwrap_or_else(|_| { + let home = env::var("HOME").expect("HOME env variable not set"); + PathBuf::from(home).join(".config") + }); + let config_dir = config_dir.join("codspeed"); + config_dir.join("config.yaml") } -#[cfg(test)] -impl Config { - /// Constructs a new `Config` with default values for testing purposes - pub fn test() -> Self { +impl Default for CodSpeedConfig { + fn default() -> Self { Self { - upload_url: Url::parse(DEFAULT_UPLOAD_URL).unwrap(), - token: None, - working_directory: None, - command: "".into(), - instruments: Instruments::test(), - skip_upload: false, - skip_setup: false, + auth: AuthConfig { token: None }, } } } -const DEFAULT_UPLOAD_URL: &str = "https://api.codspeed.io/upload"; +impl CodSpeedConfig { + /// Load the configuration. If it does not exist, store and return a default configuration + pub fn load() -> Result { + let config_path = get_configuration_file_path(); -impl TryFrom for Config { - type Error = Error; - fn try_from(args: AppArgs) -> Result { - let instruments = Instruments::try_from(&args)?; - let raw_upload_url = args.upload_url.unwrap_or_else(|| DEFAULT_UPLOAD_URL.into()); - let upload_url = Url::parse(&raw_upload_url) - .map_err(|e| anyhow!("Invalid upload URL: {}, {}", raw_upload_url, e))?; - Ok(Self { - upload_url, - token: args.token, - working_directory: args.working_directory, - instruments, - command: args.command.join(" "), - skip_upload: args.skip_upload, - skip_setup: args.skip_setup, - }) + match fs::read(&config_path) { + Ok(config_str) => { + let config = serde_yaml::from_slice(&config_str).context(format!( + "Failed to parse CodSpeed config at {}", + config_path.display() + ))?; + debug!("Config loaded from {}", config_path.display()); + Ok(config) + } + Err(e) if e.kind() == std::io::ErrorKind::NotFound => { + debug!("Config file not found at {}", config_path.display()); + let config = CodSpeedConfig::default(); + config.persist()?; + Ok(config) + } + Err(e) => bail!("Failed to load config: {}", e), + } } -} - -#[cfg(test)] -mod tests { - use crate::instruments::MongoDBConfig; - use super::*; + /// Persist changes to the configuration + pub fn persist(&self) -> Result<()> { + let config_path = get_configuration_file_path(); + fs::create_dir_all(config_path.parent().unwrap())?; - #[test] - fn test_try_from_env_empty() { - let config = Config::try_from(AppArgs { - upload_url: None, - token: None, - working_directory: None, - instruments: vec![], - mongo_uri_env_name: None, - skip_upload: false, - skip_setup: false, - command: vec!["cargo".into(), "codspeed".into(), "bench".into()], - }) - .unwrap(); - assert_eq!(config.upload_url, Url::parse(DEFAULT_UPLOAD_URL).unwrap()); - assert_eq!(config.token, None); - assert_eq!(config.working_directory, None); - assert_eq!(config.instruments, Instruments { mongodb: None }); - assert!(!config.skip_upload); - assert!(!config.skip_setup); - assert_eq!(config.command, "cargo codspeed bench"); - } - - #[test] - fn test_try_from_args() { - let config = Config::try_from(AppArgs { - upload_url: Some("https://example.com/upload".into()), - token: Some("token".into()), - working_directory: Some("/tmp".into()), - instruments: vec!["mongodb".into()], - mongo_uri_env_name: Some("MONGODB_URI".into()), - skip_upload: true, - skip_setup: true, - command: vec!["cargo".into(), "codspeed".into(), "bench".into()], - }) - .unwrap(); + let config_str = serde_yaml::to_string(self)?; + fs::write(&config_path, config_str)?; + debug!("Config written to {}", config_path.display()); - assert_eq!( - config.upload_url, - Url::parse("https://example.com/upload").unwrap() - ); - assert_eq!(config.token, Some("token".into())); - assert_eq!(config.working_directory, Some("/tmp".into())); - assert_eq!( - config.instruments, - Instruments { - mongodb: Some(MongoDBConfig { - uri_env_name: Some("MONGODB_URI".into()) - }) - } - ); - assert!(config.skip_upload); - assert!(config.skip_setup); - assert_eq!(config.command, "cargo codspeed bench"); + Ok(()) } } diff --git a/src/local_logger.rs b/src/local_logger.rs new file mode 100644 index 00000000..652d49bb --- /dev/null +++ b/src/local_logger.rs @@ -0,0 +1,166 @@ +use std::{ + env, + sync::{Arc, Mutex}, + time::Duration, +}; + +use console::{style, Style}; +use indicatif::{ProgressBar, ProgressStyle}; +use lazy_static::lazy_static; +use log::Log; +use simplelog::SharedLogger; +use std::io::Write; + +use crate::logger::{get_group_event, GroupEvent}; + +pub const CODSPEED_U8_COLOR_CODE: u8 = 208; // #FF8700 +const BLACK_U8_COLOR_CODE: u8 = 16; // #000 + +lazy_static! { + pub static ref SPINNER: Arc>> = Arc::new(Mutex::new(None)); + pub static ref IS_TTY: bool = std::io::IsTerminal::is_terminal(&std::io::stdout()); +} + +/// Hide the progress bar temporarily, execute `f`, then redraw the progress bar. +/// +/// If the output is not a TTY, `f` will be executed without hiding the progress bar. +pub fn suspend_progress_bar R, R>(f: F) -> R { + // If the output is a TTY, and there is a spinner, suspend it + if *IS_TTY { + if let Ok(mut spinner) = SPINNER.lock() { + if let Some(spinner) = spinner.as_mut() { + return spinner.suspend(f); + } + } + } + + // Otherwise, just run the function + f() +} + +pub struct LocalLogger { + log_level: log::LevelFilter, +} + +impl LocalLogger { + pub fn new() -> Self { + let log_level = env::var("CODSPEED_LOG") + .ok() + .and_then(|log_level| log_level.parse::().ok()) + .unwrap_or(log::LevelFilter::Info); + + LocalLogger { log_level } + } +} + +impl Log for LocalLogger { + fn enabled(&self, metadata: &log::Metadata) -> bool { + metadata.level() <= self.log_level + } + + fn log(&self, record: &log::Record) { + if !self.enabled(record.metadata()) { + return; + } + + if let Some(group_event) = get_group_event(record) { + match group_event { + GroupEvent::Start(name) | GroupEvent::StartOpened(name) => { + println!( + " {}", + style(format!(" {} ", name.to_uppercase())) + .bold() + .color256(BLACK_U8_COLOR_CODE) + .on_color256(CODSPEED_U8_COLOR_CODE) + ); + println!(); + + if *IS_TTY { + let spinner = ProgressBar::new_spinner(); + spinner.set_style( + ProgressStyle::with_template( + format!( + " {{spinner:>.{}}} {{wide_msg:.{}.bold}}", + CODSPEED_U8_COLOR_CODE, CODSPEED_U8_COLOR_CODE + ) + .as_str(), + ) + .unwrap(), + ); + spinner.set_message(format!("{}...", name)); + spinner.enable_steady_tick(Duration::from_millis(100)); + SPINNER.lock().unwrap().replace(spinner); + } else { + println!("{}...", name); + } + } + GroupEvent::End => { + if *IS_TTY { + let mut spinner = SPINNER.lock().unwrap(); + if let Some(spinner) = spinner.as_mut() { + spinner.finish_and_clear(); + println!(); + } + } + println!(); + } + } + + return; + } + + suspend_progress_bar(|| print_record(record)); + } + + fn flush(&self) { + std::io::stdout().flush().unwrap(); + } +} + +/// Print a log record to the console with the appropriate style +fn print_record(record: &log::Record) { + let error_style = Style::new().red(); + let info_style = Style::new().white(); + let warn_style = Style::new().yellow(); + let debug_style = Style::new().blue().dim(); + let trace_style = Style::new().black().dim(); + + match record.level() { + log::Level::Error => eprintln!("{}", error_style.apply_to(record.args())), + log::Level::Warn => eprintln!("{}", warn_style.apply_to(record.args())), + log::Level::Info => println!("{}", info_style.apply_to(record.args())), + log::Level::Debug => println!( + "{}", + debug_style.apply_to(format!("[DEBUG::{}] {}", record.target(), record.args())), + ), + log::Level::Trace => println!( + "{}", + trace_style.apply_to(format!("[TRACE::{}] {}", record.target(), record.args())) + ), + } +} + +impl SharedLogger for LocalLogger { + fn level(&self) -> log::LevelFilter { + self.log_level + } + + fn config(&self) -> Option<&simplelog::Config> { + None + } + + fn as_log(self: Box) -> Box { + Box::new(*self) + } +} + +pub fn get_local_logger() -> Box { + Box::new(LocalLogger::new()) +} + +pub fn clean_logger() { + let mut spinner = SPINNER.lock().unwrap(); + if let Some(spinner) = spinner.as_mut() { + spinner.finish_and_clear(); + } +} diff --git a/src/logger.rs b/src/logger.rs index beeed012..09332385 100644 --- a/src/logger.rs +++ b/src/logger.rs @@ -1,39 +1,73 @@ -use crate::ci_provider::logger::{GROUP_TARGET, OPENED_GROUP_TARGET}; -use crate::runner::RunData; -use crate::{ci_provider::CIProvider, prelude::*}; -use log::LevelFilter; -use simplelog::{CombinedLogger, WriteLogger}; -use std::fs::copy; -use std::path::PathBuf; -use tempfile::NamedTempFile; +/// This target is used exclusively to handle group events. +pub const GROUP_TARGET: &str = "codspeed::group"; +pub const OPENED_GROUP_TARGET: &str = "codspeed::group::opened"; -pub struct Logger { - log_file_path: PathBuf, +#[macro_export] +/// Start a new log group. All logs between this and the next `end_group!` will be grouped together. +/// +/// # Example +/// +/// ```rust +/// start_group!("My group"); +/// info!("This will be grouped"); +/// end_group!(); +/// ``` +macro_rules! start_group { + ($name:expr) => { + log::log!(target: $crate::logger::GROUP_TARGET, log::Level::Info, "{}", $name); + }; } -impl Logger { - #[allow(clippy::borrowed_box)] - pub fn new(provider: &Box) -> Result { - let provider_logger = provider.get_logger(); - let log_file = NamedTempFile::new().context("Failed to create log file")?; - let log_file_path = log_file.path().to_path_buf(); - let file_logger_config = simplelog::ConfigBuilder::new() - // Groups are not logged to the file - .add_filter_ignore_str(GROUP_TARGET) - .add_filter_ignore_str(OPENED_GROUP_TARGET) - .build(); - let file_logger = WriteLogger::new(LevelFilter::Trace, file_logger_config, log_file); - CombinedLogger::init(vec![provider_logger, file_logger]) - .context("Failed to init logger")?; - Ok(Self { log_file_path }) - } +#[macro_export] +/// Start a new opened log group. All logs between this and the next `end_group!` will be grouped together. +/// +/// # Example +/// +/// ```rust +/// start_opened_group!("My group"); +/// info!("This will be grouped"); +/// end_group!(); +/// ``` +macro_rules! start_opened_group { + ($name:expr) => { + log::log!(target: $crate::logger::OPENED_GROUP_TARGET, log::Level::Info, "{}", $name); + }; +} + +#[macro_export] +/// End the current log group. +/// See [`start_group!`] for more information. +macro_rules! end_group { + () => { + log::log!(target: $crate::logger::GROUP_TARGET, log::Level::Info, ""); + }; +} + +pub enum GroupEvent { + Start(String), + StartOpened(String), + End, +} - pub fn persist_log_to_profile_folder(&self, run_data: &RunData) -> Result<()> { - let profile_folder = run_data.profile_folder.clone(); - let dest_log_file_path = profile_folder.join("runner.log"); - debug!("Persisting log file to {}", dest_log_file_path.display()); - log::logger().flush(); - copy(&self.log_file_path, dest_log_file_path).context("Failed to copy log file")?; - Ok(()) +/// Returns the group event if the record is a group event, otherwise returns `None`. +pub(super) fn get_group_event(record: &log::Record) -> Option { + match record.target() { + OPENED_GROUP_TARGET => { + let args = record.args().to_string(); + if args.is_empty() { + None + } else { + Some(GroupEvent::StartOpened(args)) + } + } + GROUP_TARGET => { + let args = record.args().to_string(); + if args.is_empty() { + Some(GroupEvent::End) + } else { + Some(GroupEvent::Start(args)) + } + } + _ => None, } } diff --git a/src/main.rs b/src/main.rs index 75a8a736..b9bfac5b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,14 +1,15 @@ +mod api_client; mod app; -mod ci_provider; +mod auth; mod config; -mod helpers; -mod instruments; +mod local_logger; mod logger; mod prelude; mod request_client; -mod runner; -mod uploader; +mod run; +use console::style; +use local_logger::clean_logger; use prelude::*; use log::log_enabled; @@ -21,16 +22,20 @@ pub const VALGRIND_CODSPEED_VERSION: &str = "3.21.0-0codspeed1"; async fn main() { let res = crate::app::run().await; if let Err(err) = res { - if log_enabled!(log::Level::Error) { - error!("Error {}", err); - } else { - eprintln!("Error {}", err); + for cause in err.chain() { + if log_enabled!(log::Level::Error) { + error!("{} {}", style("Error:").bold().red(), style(cause).red()); + } else { + eprintln!("Error: {}", cause); + } } if log_enabled!(log::Level::Debug) { for e in err.chain().skip(1) { debug!("Caused by: {}", e); } } + clean_logger(); + std::process::exit(1); } } diff --git a/src/queries/ConsumeLoginSession.gql b/src/queries/ConsumeLoginSession.gql new file mode 100644 index 00000000..b2461722 --- /dev/null +++ b/src/queries/ConsumeLoginSession.gql @@ -0,0 +1,5 @@ +mutation ConsumeLoginSession($sessionId: String!) { + consumeLoginSession(sessionId: $sessionId) { + token + } +} diff --git a/src/queries/CreateLoginSession.gql b/src/queries/CreateLoginSession.gql new file mode 100644 index 00000000..77f2dbb9 --- /dev/null +++ b/src/queries/CreateLoginSession.gql @@ -0,0 +1,6 @@ +mutation CreateLoginSession { + createLoginSession { + callbackUrl + sessionId + } +} diff --git a/src/queries/FetchLocalRunReport.gql b/src/queries/FetchLocalRunReport.gql new file mode 100644 index 00000000..20e8f61a --- /dev/null +++ b/src/queries/FetchLocalRunReport.gql @@ -0,0 +1,17 @@ +query FetchLocalRunReport($owner: String!, $name: String!, $runId: String!) { + repository(owner: $owner, name: $name) { + settings { + allowedRegression + } + runs(where: { id: { equals: $runId } }) { + id + status + url + headReports { + id + impact + conclusion + } + } + } +} diff --git a/src/run/check_system.rs b/src/run/check_system.rs new file mode 100644 index 00000000..ffd182bd --- /dev/null +++ b/src/run/check_system.rs @@ -0,0 +1,83 @@ +use std::process::Command; + +use serde::{Deserialize, Serialize}; +use sysinfo::System; + +use crate::prelude::*; + +fn get_user() -> Result { + let user_output = Command::new("whoami") + .output() + .map_err(|_| anyhow!("Failed to get user info"))?; + if !user_output.status.success() { + bail!("Failed to get user info"); + } + let output_str = + String::from_utf8(user_output.stdout).map_err(|_| anyhow!("Failed to parse user info"))?; + Ok(output_str.trim().to_string()) +} + +#[derive(Eq, PartialEq, Hash, Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct SystemInfo { + pub os: String, + pub os_version: String, + pub arch: String, + pub host: String, + pub user: String, +} + +#[cfg(test)] +impl SystemInfo { + pub fn test() -> Self { + SystemInfo { + os: "Ubuntu".to_string(), + os_version: "20.04".to_string(), + arch: "x86_64".to_string(), + host: "host".to_string(), + user: "user".to_string(), + } + } +} + +impl SystemInfo { + fn new() -> Result { + let os = System::name().ok_or(anyhow!("Failed to get OS name"))?; + let os_version = System::os_version().ok_or(anyhow!("Failed to get OS version"))?; + let arch = System::cpu_arch().ok_or(anyhow!("Failed to get CPU architecture"))?; + let user = get_user()?; + let host = System::host_name().ok_or(anyhow!("Failed to get host name"))?; + + Ok(SystemInfo { + os, + os_version, + arch, + host, + user, + }) + } +} + +/// Checks if the system is supported and returns the system info +/// +/// Supported systems: +/// - Ubuntu 20.04 on x86_64 +/// - Ubuntu 22.04 on x86_64 +/// - Debian 11 on x86_64 +/// - Debian 12 on x86_64 +pub fn check_system() -> Result { + let system_info = SystemInfo::new()?; + debug!("System info: {:#?}", system_info); + + match (system_info.os.as_str(), system_info.os_version.as_str()) { + ("Ubuntu", "20.04") | ("Ubuntu", "22.04") | ("Debian", "11") | ("Debian", "12") => (), + ("Ubuntu", _) => bail!("Only Ubuntu 20.04 and 22.04 are supported at the moment"), + ("Debian", _) => bail!("Only Debian 11 and 12 are supported at the moment"), + _ => bail!("Only Ubuntu and Debian are supported at the moment"), + } + if system_info.arch != "x86_64" { + bail!("Only x86_64 is supported at the moment"); + } + + Ok(system_info) +} diff --git a/src/ci_provider/buildkite/logger.rs b/src/run/ci_provider/buildkite/logger.rs similarity index 94% rename from src/ci_provider/buildkite/logger.rs rename to src/run/ci_provider/buildkite/logger.rs index 622deb64..df727c58 100644 --- a/src/ci_provider/buildkite/logger.rs +++ b/src/run/ci_provider/buildkite/logger.rs @@ -1,5 +1,6 @@ -use crate::ci_provider::logger::{ - get_group_event, should_provider_logger_handle_record, GroupEvent, +use crate::{ + logger::{get_group_event, GroupEvent}, + run::ci_provider::logger::should_provider_logger_handle_record, }; use log::*; use simplelog::SharedLogger; diff --git a/src/ci_provider/buildkite/mod.rs b/src/run/ci_provider/buildkite/mod.rs similarity index 100% rename from src/ci_provider/buildkite/mod.rs rename to src/run/ci_provider/buildkite/mod.rs diff --git a/src/ci_provider/buildkite/provider.rs b/src/run/ci_provider/buildkite/provider.rs similarity index 97% rename from src/ci_provider/buildkite/provider.rs rename to src/run/ci_provider/buildkite/provider.rs index 322e4640..1dac7102 100644 --- a/src/ci_provider/buildkite/provider.rs +++ b/src/run/ci_provider/buildkite/provider.rs @@ -4,14 +4,14 @@ use lazy_static::lazy_static; use regex::Regex; use simplelog::SharedLogger; -use crate::{ +use crate::prelude::*; +use crate::run::{ ci_provider::{ interfaces::{ProviderMetadata, RunEvent}, provider::{CIProvider, CIProviderDetector}, }, config::Config, helpers::{find_repository_root, get_env_variable}, - prelude::*, }; use super::logger::BuildkiteLogger; @@ -23,7 +23,6 @@ pub struct BuildkiteProvider { pub ref_: String, pub head_ref: Option, pub base_ref: Option, - pub commit_hash: String, pub event: RunEvent, pub repository_root_path: String, } @@ -121,7 +120,6 @@ impl TryFrom<&Config> for BuildkiteProvider { } else { None }, - commit_hash: get_env_variable("BUILDKITE_COMMIT")?, event: get_run_event()?, repository_root_path, }) @@ -230,7 +228,6 @@ mod tests { assert_eq!(provider.ref_, "refs/heads/main"); assert_eq!(provider.base_ref, None); assert_eq!(provider.head_ref, None); - assert_eq!(provider.commit_hash, "abc123"); assert_eq!(provider.event, RunEvent::Push); assert_eq!( provider.repository_root_path, @@ -270,7 +267,6 @@ mod tests { assert_eq!(provider.ref_, "refs/pull/22/merge"); assert_eq!(provider.base_ref, Some("main".into())); assert_eq!(provider.head_ref, Some("feat/codspeed-runner".into())); - assert_eq!(provider.commit_hash, "abc123"); assert_eq!(provider.event, RunEvent::PullRequest); assert_eq!( provider.repository_root_path, diff --git a/src/ci_provider/buildkite/snapshots/codspeed_runner__ci_provider__buildkite__provider__tests__pull_request_provider_metadata.snap b/src/run/ci_provider/buildkite/snapshots/codspeed__run__ci_provider__buildkite__provider__tests__pull_request_provider_metadata.snap similarity index 86% rename from src/ci_provider/buildkite/snapshots/codspeed_runner__ci_provider__buildkite__provider__tests__pull_request_provider_metadata.snap rename to src/run/ci_provider/buildkite/snapshots/codspeed__run__ci_provider__buildkite__provider__tests__pull_request_provider_metadata.snap index dc325f93..28f1e6b1 100644 --- a/src/ci_provider/buildkite/snapshots/codspeed_runner__ci_provider__buildkite__provider__tests__pull_request_provider_metadata.snap +++ b/src/run/ci_provider/buildkite/snapshots/codspeed__run__ci_provider__buildkite__provider__tests__pull_request_provider_metadata.snap @@ -1,5 +1,5 @@ --- -source: src/ci_provider/buildkite/provider.rs +source: src/run/ci_provider/buildkite/provider.rs expression: provider_metadata --- { diff --git a/src/ci_provider/github_actions/logger.rs b/src/run/ci_provider/github_actions/logger.rs similarity index 94% rename from src/ci_provider/github_actions/logger.rs rename to src/run/ci_provider/github_actions/logger.rs index 59f35c05..5a24f870 100644 --- a/src/ci_provider/github_actions/logger.rs +++ b/src/run/ci_provider/github_actions/logger.rs @@ -1,5 +1,6 @@ -use crate::ci_provider::logger::{ - get_group_event, should_provider_logger_handle_record, GroupEvent, +use crate::{ + logger::{get_group_event, GroupEvent}, + run::ci_provider::logger::should_provider_logger_handle_record, }; use log::*; use simplelog::SharedLogger; diff --git a/src/ci_provider/github_actions/mod.rs b/src/run/ci_provider/github_actions/mod.rs similarity index 100% rename from src/ci_provider/github_actions/mod.rs rename to src/run/ci_provider/github_actions/mod.rs diff --git a/src/ci_provider/github_actions/provider.rs b/src/run/ci_provider/github_actions/provider.rs similarity index 92% rename from src/ci_provider/github_actions/provider.rs rename to src/run/ci_provider/github_actions/provider.rs index 727e32e3..cfa6b5c4 100644 --- a/src/ci_provider/github_actions/provider.rs +++ b/src/run/ci_provider/github_actions/provider.rs @@ -4,14 +4,14 @@ use serde_json::Value; use simplelog::SharedLogger; use std::{env, fs}; -use crate::{ +use crate::prelude::*; +use crate::run::{ ci_provider::{ interfaces::{GhData, ProviderMetadata, RunEvent, Sender}, provider::{CIProvider, CIProviderDetector}, }, config::Config, helpers::{find_repository_root, get_env_variable}, - prelude::*, }; use super::logger::GithubActionLogger; @@ -23,7 +23,6 @@ pub struct GitHubActionsProvider { pub ref_: String, pub head_ref: Option, pub base_ref: Option, - pub commit_hash: String, pub gh_data: GhData, pub event: RunEvent, pub repository_root_path: String, @@ -49,7 +48,7 @@ impl TryFrom<&Config> for GitHubActionsProvider { let (owner, repository) = Self::get_owner_and_repository()?; let ref_ = get_env_variable("GITHUB_REF")?; let is_pr = PR_REF_REGEX.is_match(&ref_); - let (head_ref, commit_hash) = if is_pr { + let head_ref = if is_pr { let github_event_path = get_env_variable("GITHUB_EVENT_PATH")?; let github_event = fs::read_to_string(github_event_path)?; let github_event: Value = serde_json::from_str(&github_event) @@ -70,12 +69,9 @@ impl TryFrom<&Config> for GitHubActionsProvider { } else { pull_request["head"]["ref"].as_str().unwrap().to_owned() }; - ( - Some(head_ref), - pull_request["head"]["sha"].as_str().unwrap().to_owned(), - ) + Some(head_ref) } else { - (None, get_env_variable("GITHUB_SHA")?) + None }; let github_event_name = get_env_variable("GITHUB_EVENT_NAME")?; @@ -95,7 +91,6 @@ impl TryFrom<&Config> for GitHubActionsProvider { owner, repository: repository.clone(), ref_, - commit_hash, head_ref, event, gh_data: GhData { @@ -187,7 +182,6 @@ mod tests { ("GITHUB_REF", Some("refs/heads/main")), ("GITHUB_REPOSITORY", Some("owner/repository")), ("GITHUB_RUN_ID", Some("1234567890")), - ("GITHUB_SHA", Some("1234567890abcdef")), ], || { let config = Config { @@ -200,7 +194,6 @@ mod tests { assert_eq!(github_actions_provider.ref_, "refs/heads/main"); assert_eq!(github_actions_provider.base_ref, Some("main".into())); assert_eq!(github_actions_provider.head_ref, None); - assert_eq!(github_actions_provider.commit_hash, "1234567890abcdef"); assert_eq!(github_actions_provider.event, RunEvent::Push); assert_eq!(github_actions_provider.gh_data.job, "job"); assert_eq!(github_actions_provider.gh_data.run_id, 1234567890); @@ -234,7 +227,7 @@ mod tests { "GITHUB_EVENT_PATH", Some( format!( - "{}/src/ci_provider/github_actions/samples/pr-event.json", + "{}/src/run/ci_provider/github_actions/samples/pr-event.json", env!("CARGO_MANIFEST_DIR") ) .as_str(), @@ -245,10 +238,6 @@ mod tests { ("GITHUB_REF", Some("refs/pull/22/merge")), ("GITHUB_REPOSITORY", Some("my-org/adrien-python-test")), ("GITHUB_RUN_ID", Some("6957110437")), - ( - "GITHUB_SHA", - Some("5bd77cb0da72bef094893ed45fb793ff16ecfbe3"), - ), ("VERSION", Some("0.1.0")), ], || { @@ -282,7 +271,7 @@ mod tests { "GITHUB_EVENT_PATH", Some( format!( - "{}/src/ci_provider/github_actions/samples/fork-pr-event.json", + "{}/src/run/ci_provider/github_actions/samples/fork-pr-event.json", env!("CARGO_MANIFEST_DIR") ) .as_str(), @@ -293,10 +282,6 @@ mod tests { ("GITHUB_REF", Some("refs/pull/22/merge")), ("GITHUB_REPOSITORY", Some("my-org/adrien-python-test")), ("GITHUB_RUN_ID", Some("6957110437")), - ( - "GITHUB_SHA", - Some("5bd77cb0da72bef094893ed45fb793ff16ecfbe3"), - ), ("VERSION", Some("0.1.0")), ], || { diff --git a/src/ci_provider/github_actions/samples/fork-pr-event.json b/src/run/ci_provider/github_actions/samples/fork-pr-event.json similarity index 100% rename from src/ci_provider/github_actions/samples/fork-pr-event.json rename to src/run/ci_provider/github_actions/samples/fork-pr-event.json diff --git a/src/ci_provider/github_actions/samples/pr-event.json b/src/run/ci_provider/github_actions/samples/pr-event.json similarity index 100% rename from src/ci_provider/github_actions/samples/pr-event.json rename to src/run/ci_provider/github_actions/samples/pr-event.json diff --git a/src/ci_provider/github_actions/snapshots/codspeed_runner__ci_provider__github_actions__provider__tests__fork_pull_request_provider_metadata.snap b/src/run/ci_provider/github_actions/snapshots/codspeed__run__ci_provider__github_actions__provider__tests__fork_pull_request_provider_metadata.snap similarity index 89% rename from src/ci_provider/github_actions/snapshots/codspeed_runner__ci_provider__github_actions__provider__tests__fork_pull_request_provider_metadata.snap rename to src/run/ci_provider/github_actions/snapshots/codspeed__run__ci_provider__github_actions__provider__tests__fork_pull_request_provider_metadata.snap index 8b8d6ef3..f6c9bbba 100644 --- a/src/ci_provider/github_actions/snapshots/codspeed_runner__ci_provider__github_actions__provider__tests__fork_pull_request_provider_metadata.snap +++ b/src/run/ci_provider/github_actions/snapshots/codspeed__run__ci_provider__github_actions__provider__tests__fork_pull_request_provider_metadata.snap @@ -1,5 +1,5 @@ --- -source: src/ci_provider/github_actions/provider.rs +source: src/run/ci_provider/github_actions/provider.rs expression: provider_metadata --- { diff --git a/src/ci_provider/github_actions/snapshots/codspeed_runner__ci_provider__github_actions__provider__tests__pull_request_provider_metadata.snap b/src/run/ci_provider/github_actions/snapshots/codspeed__run__ci_provider__github_actions__provider__tests__pull_request_provider_metadata.snap similarity index 88% rename from src/ci_provider/github_actions/snapshots/codspeed_runner__ci_provider__github_actions__provider__tests__pull_request_provider_metadata.snap rename to src/run/ci_provider/github_actions/snapshots/codspeed__run__ci_provider__github_actions__provider__tests__pull_request_provider_metadata.snap index 9ca01187..a4b3d7fa 100644 --- a/src/ci_provider/github_actions/snapshots/codspeed_runner__ci_provider__github_actions__provider__tests__pull_request_provider_metadata.snap +++ b/src/run/ci_provider/github_actions/snapshots/codspeed__run__ci_provider__github_actions__provider__tests__pull_request_provider_metadata.snap @@ -1,5 +1,5 @@ --- -source: src/ci_provider/github_actions/provider.rs +source: src/run/ci_provider/github_actions/provider.rs expression: provider_metadata --- { diff --git a/src/ci_provider/interfaces.rs b/src/run/ci_provider/interfaces.rs similarity index 98% rename from src/ci_provider/interfaces.rs rename to src/run/ci_provider/interfaces.rs index d9fb7291..e802da84 100644 --- a/src/ci_provider/interfaces.rs +++ b/src/run/ci_provider/interfaces.rs @@ -21,6 +21,7 @@ pub enum RunEvent { PullRequest, WorkflowDispatch, Schedule, + Local, } #[derive(Deserialize, Serialize, Debug, Clone)] diff --git a/src/run/ci_provider/local/mod.rs b/src/run/ci_provider/local/mod.rs new file mode 100644 index 00000000..78222953 --- /dev/null +++ b/src/run/ci_provider/local/mod.rs @@ -0,0 +1,3 @@ +mod provider; + +pub use provider::LocalProvider; diff --git a/src/run/ci_provider/local/provider.rs b/src/run/ci_provider/local/provider.rs new file mode 100644 index 00000000..f0cbf71b --- /dev/null +++ b/src/run/ci_provider/local/provider.rs @@ -0,0 +1,165 @@ +use git2::Repository; +use lazy_static::lazy_static; +use simplelog::SharedLogger; + +use crate::local_logger::get_local_logger; +use crate::prelude::*; +use crate::run::{ + ci_provider::{ + interfaces::{ProviderMetadata, RunEvent}, + provider::{CIProvider, CIProviderDetector}, + }, + config::Config, + helpers::find_repository_root, +}; + +#[derive(Debug)] +pub struct LocalProvider { + pub ref_: String, + pub owner: String, + pub repository: String, + pub head_ref: Option, + pub base_ref: Option, + pub event: RunEvent, + pub repository_root_path: String, +} + +impl LocalProvider {} + +lazy_static! { + static ref REMOTE_REGEX: regex::Regex = + regex::Regex::new(r"[:/](?P[^/]+)/(?P[^/]+)\.git").unwrap(); +} + +fn extract_owner_and_repository_from_remote_url(remote_url: &str) -> Result<(String, String)> { + let captures = REMOTE_REGEX.captures(remote_url).ok_or_else(|| { + anyhow!( + "Could not extract owner and repository from remote url: {}", + remote_url + ) + })?; + + let owner = captures.name("owner").unwrap().as_str(); + let repository = captures.name("repository").unwrap().as_str(); + + Ok((owner.to_string(), repository.to_string())) +} + +impl TryFrom<&Config> for LocalProvider { + type Error = Error; + fn try_from(_config: &Config) -> Result { + let repository_root_path = match find_repository_root(&std::env::current_dir()?) { + Some(mut path) => { + // Add a trailing slash to the path + path.push(""); + path.to_string_lossy().to_string() + }, + None => bail!("Could not find repository root, please make sure you are running the command from inside a git repository"), + }; + + let git_repository = Repository::open(repository_root_path.clone()).context(format!( + "Failed to open repository at path: {}", + repository_root_path + ))?; + + let remote = git_repository.find_remote("origin")?; + let (owner, repository) = + extract_owner_and_repository_from_remote_url(remote.url().unwrap())?; + + let head = git_repository.head().context("Failed to get HEAD")?; + let ref_ = head + .peel_to_commit() + .context("Failed to get HEAD commit")? + .id() + .to_string(); + let head_ref = if head.is_branch() { + let branch = head.shorthand().context("Failed to get HEAD branch name")?; + Some(branch.to_string()) + } else { + None + }; + + Ok(Self { + ref_, + head_ref, + base_ref: None, + owner, + repository, + event: RunEvent::Local, + repository_root_path, + }) + } +} + +impl CIProviderDetector for LocalProvider { + fn detect() -> bool { + true + } +} + +impl CIProvider for LocalProvider { + fn get_logger(&self) -> Box { + get_local_logger() + } + + fn get_provider_name(&self) -> &'static str { + "Local" + } + + fn get_provider_slug(&self) -> &'static str { + "local" + } + + fn get_provider_metadata(&self) -> Result { + Ok(ProviderMetadata { + base_ref: self.base_ref.clone(), + head_ref: self.head_ref.clone(), + event: self.event.clone(), + gh_data: None, + owner: self.owner.clone(), + repository: self.repository.clone(), + ref_: self.ref_.clone(), + repository_root_path: self.repository_root_path.clone(), + }) + } +} + +#[cfg(test)] +mod tests { + // use crate::VERSION; + // use insta::assert_json_snapshot; + + use super::*; + + #[test] + fn test_extract_owner_and_repository_from_remote_url() { + let remote_urls = [ + "git@github.com:CodSpeedHQ/runner.git", + "https://github.com/CodSpeedHQ/runner.git", + ]; + for remote_url in remote_urls.iter() { + let (owner, repository) = + extract_owner_and_repository_from_remote_url(remote_url).unwrap(); + assert_eq!(owner, "CodSpeedHQ"); + assert_eq!(repository, "runner"); + } + } + + // TODO: uncomment later when we have a way to mock git repository + // #[test] + // fn test_provider_metadata() { + // let config = Config { + // token: Some("token".into()), + // ..Config::test() + // }; + // let local_provider = LocalProvider::try_from(&config).unwrap(); + // let provider_metadata = local_provider.get_provider_metadata().unwrap(); + + // assert_json_snapshot!(provider_metadata, { + // ".runner.version" => insta::dynamic_redaction(|value,_path| { + // assert_eq!(value.as_str().unwrap(), VERSION.to_string()); + // "[version]" + // }), + // }); + // } +} diff --git a/src/run/ci_provider/local/snapshots/codspeed_runner__run__ci_provider__local__provider__tests__provider_metadata.snap b/src/run/ci_provider/local/snapshots/codspeed_runner__run__ci_provider__local__provider__tests__provider_metadata.snap new file mode 100644 index 00000000..73882345 --- /dev/null +++ b/src/run/ci_provider/local/snapshots/codspeed_runner__run__ci_provider__local__provider__tests__provider_metadata.snap @@ -0,0 +1,12 @@ +--- +source: src/run/ci_provider/local/provider.rs +expression: provider_metadata +--- +{ + "ref": "18ec1d64b5f25fb27451d89eee03cc569bd6bbb1", + "headRef": "feat/branch", + "owner": "my-org", + "repository": "adrien-python-test", + "event": "local", + "repositoryRootPath": "/Users/adrien/projects/my-org/adrien-python-test" +} diff --git a/src/run/ci_provider/logger.rs b/src/run/ci_provider/logger.rs new file mode 100644 index 00000000..85ba7338 --- /dev/null +++ b/src/run/ci_provider/logger.rs @@ -0,0 +1,6 @@ +use crate::run::runner::VALGRIND_EXECUTION_TARGET; + +pub(super) fn should_provider_logger_handle_record(record: &log::Record) -> bool { + // Provider logger should handle all records except the ones from the valgrind execution target + record.target() != VALGRIND_EXECUTION_TARGET +} diff --git a/src/ci_provider/mod.rs b/src/run/ci_provider/mod.rs similarity index 64% rename from src/ci_provider/mod.rs rename to src/run/ci_provider/mod.rs index 18280eae..ae06ed80 100644 --- a/src/ci_provider/mod.rs +++ b/src/run/ci_provider/mod.rs @@ -2,17 +2,20 @@ pub mod interfaces; pub mod logger; mod provider; -use crate::ci_provider::buildkite::BuildkiteProvider; -use crate::ci_provider::github_actions::GitHubActionsProvider; -use crate::ci_provider::provider::CIProviderDetector; -use crate::config::Config; +use buildkite::BuildkiteProvider; +use github_actions::GitHubActionsProvider; +use local::LocalProvider; +use provider::CIProviderDetector; + use crate::prelude::*; +use crate::run::config::Config; pub use self::provider::CIProvider; // Provider implementations mod buildkite; mod github_actions; +mod local; pub fn get_provider(config: &Config) -> Result> { if BuildkiteProvider::detect() { @@ -25,5 +28,10 @@ pub fn get_provider(config: &Config) -> Result> { return Ok(Box::new(provider)); } + if LocalProvider::detect() { + let provider = LocalProvider::try_from(config)?; + return Ok(Box::new(provider)); + } + bail!("No CI provider detected") } diff --git a/src/ci_provider/provider.rs b/src/run/ci_provider/provider.rs similarity index 87% rename from src/ci_provider/provider.rs rename to src/run/ci_provider/provider.rs index 32b2636a..fb581b4b 100644 --- a/src/ci_provider/provider.rs +++ b/src/run/ci_provider/provider.rs @@ -1,9 +1,10 @@ use git2::Repository; use simplelog::SharedLogger; -use crate::config::Config; use crate::prelude::*; -use crate::uploader::{Runner, UploadMetadata}; +use crate::run::check_system::SystemInfo; +use crate::run::config::Config; +use crate::run::uploader::{Runner, UploadMetadata}; use super::interfaces::ProviderMetadata; @@ -19,7 +20,8 @@ fn get_commit_hash(repository_root_path: &str) -> Result { ))?; let commit_hash = repo - .revparse_single("HEAD") + .head() + .and_then(|head| head.peel_to_commit()) .context("Failed to get HEAD commit")? .id() .to_string(); @@ -70,13 +72,18 @@ pub trait CIProvider { /// let instruments = Instruments::new(); /// let metadata = provider.get_upload_metadata(&config, "abc123").unwrap(); /// ``` - fn get_upload_metadata(&self, config: &Config, archive_hash: &str) -> Result { + fn get_upload_metadata( + &self, + config: &Config, + system_info: &SystemInfo, + archive_hash: &str, + ) -> Result { let provider_metadata = self.get_provider_metadata()?; let commit_hash = get_commit_hash(&provider_metadata.repository_root_path)?; Ok(UploadMetadata { - version: Some(2), + version: Some(3), tokenless: config.token.is_none(), provider_metadata, profile_md5: archive_hash.into(), @@ -85,6 +92,7 @@ pub trait CIProvider { name: "codspeed-runner".into(), version: crate::VERSION.into(), instruments: config.instruments.get_active_instrument_names(), + system_info: system_info.clone(), }, platform: self.get_provider_slug().into(), }) diff --git a/src/run/config.rs b/src/run/config.rs new file mode 100644 index 00000000..6ab959a6 --- /dev/null +++ b/src/run/config.rs @@ -0,0 +1,123 @@ +use crate::prelude::*; +use crate::run::instruments::Instruments; +use url::Url; + +use crate::run::RunArgs; + +#[derive(Debug)] +pub struct Config { + pub upload_url: Url, + pub token: Option, + pub working_directory: Option, + pub command: String, + + pub instruments: Instruments, + + pub skip_upload: bool, + pub skip_setup: bool, +} + +impl Config { + pub fn set_token(&mut self, token: Option) { + self.token = token; + } +} + +#[cfg(test)] +impl Config { + /// Constructs a new `Config` with default values for testing purposes + pub fn test() -> Self { + Self { + upload_url: Url::parse(DEFAULT_UPLOAD_URL).unwrap(), + token: None, + working_directory: None, + command: "".into(), + instruments: Instruments::test(), + skip_upload: false, + skip_setup: false, + } + } +} + +const DEFAULT_UPLOAD_URL: &str = "https://api.codspeed.io/upload"; + +impl TryFrom for Config { + type Error = Error; + fn try_from(args: RunArgs) -> Result { + let instruments = Instruments::try_from(&args)?; + let raw_upload_url = args.upload_url.unwrap_or_else(|| DEFAULT_UPLOAD_URL.into()); + let upload_url = Url::parse(&raw_upload_url) + .map_err(|e| anyhow!("Invalid upload URL: {}, {}", raw_upload_url, e))?; + Ok(Self { + upload_url, + token: args.token, + working_directory: args.working_directory, + instruments, + command: args.command.join(" "), + skip_upload: args.skip_upload, + skip_setup: args.skip_setup, + }) + } +} + +#[cfg(test)] +mod tests { + use crate::run::instruments::MongoDBConfig; + + use super::*; + + #[test] + fn test_try_from_env_empty() { + let config = Config::try_from(RunArgs { + upload_url: None, + token: None, + working_directory: None, + instruments: vec![], + mongo_uri_env_name: None, + skip_upload: false, + skip_setup: false, + command: vec!["cargo".into(), "codspeed".into(), "bench".into()], + }) + .unwrap(); + assert_eq!(config.upload_url, Url::parse(DEFAULT_UPLOAD_URL).unwrap()); + assert_eq!(config.token, None); + assert_eq!(config.working_directory, None); + assert_eq!(config.instruments, Instruments { mongodb: None }); + assert!(!config.skip_upload); + assert!(!config.skip_setup); + assert_eq!(config.command, "cargo codspeed bench"); + } + + #[test] + fn test_try_from_args() { + let config = Config::try_from(RunArgs { + upload_url: Some("https://example.com/upload".into()), + token: Some("token".into()), + working_directory: Some("/tmp".into()), + instruments: vec!["mongodb".into()], + mongo_uri_env_name: Some("MONGODB_URI".into()), + skip_upload: true, + skip_setup: true, + command: vec!["cargo".into(), "codspeed".into(), "bench".into()], + }) + .unwrap(); + + assert_eq!( + config.upload_url, + Url::parse("https://example.com/upload").unwrap() + ); + assert_eq!(config.token, Some("token".into())); + assert_eq!(config.working_directory, Some("/tmp".into())); + assert_eq!( + config.instruments, + Instruments { + mongodb: Some(MongoDBConfig { + uri_env_name: Some("MONGODB_URI".into()) + }) + } + ); + assert!(config.skip_upload); + assert!(config.skip_setup); + assert_eq!(config.command, "cargo codspeed bench"); + } +} diff --git a/src/helpers/find_repository_root.rs b/src/run/helpers/find_repository_root.rs similarity index 100% rename from src/helpers/find_repository_root.rs rename to src/run/helpers/find_repository_root.rs diff --git a/src/helpers/get_env_var.rs b/src/run/helpers/get_env_var.rs similarity index 100% rename from src/helpers/get_env_var.rs rename to src/run/helpers/get_env_var.rs diff --git a/src/helpers/mod.rs b/src/run/helpers/mod.rs similarity index 100% rename from src/helpers/mod.rs rename to src/run/helpers/mod.rs diff --git a/src/instruments/mod.rs b/src/run/instruments/mod.rs similarity index 90% rename from src/instruments/mod.rs rename to src/run/instruments/mod.rs index 9ad39b2f..8e2e79bb 100644 --- a/src/instruments/mod.rs +++ b/src/run/instruments/mod.rs @@ -3,8 +3,8 @@ use std::collections::HashSet; use log::warn; use serde::{Deserialize, Serialize}; -use crate::app::AppArgs; use crate::prelude::*; +use crate::run::RunArgs; pub mod mongo_tracer; @@ -39,9 +39,9 @@ impl Instruments { } } -impl TryFrom<&AppArgs> for Instruments { +impl TryFrom<&RunArgs> for Instruments { type Error = Error; - fn try_from(args: &AppArgs) -> Result { + fn try_from(args: &RunArgs) -> Result { let mut validated_instrument_names: HashSet = HashSet::new(); for instrument_name in &args.instruments { @@ -84,16 +84,16 @@ mod tests { #[test] fn test_from_args_empty() { - let instruments = Instruments::try_from(&AppArgs::test()).unwrap(); + let instruments = Instruments::try_from(&RunArgs::test()).unwrap(); assert!(instruments.mongodb.is_none()); } #[test] fn test_from_args() { - let args = AppArgs { + let args = RunArgs { instruments: vec!["mongodb".into()], mongo_uri_env_name: Some("MONGODB_URI".into()), - ..AppArgs::test() + ..RunArgs::test() }; let instruments = Instruments::try_from(&args).unwrap(); assert_eq!( @@ -107,10 +107,10 @@ mod tests { #[test] fn test_from_args_mongodb_disabled() { - let args = AppArgs { + let args = RunArgs { instruments: vec![], mongo_uri_env_name: Some("MONGODB_URI".into()), - ..AppArgs::test() + ..RunArgs::test() }; let instruments = Instruments::try_from(&args).unwrap(); assert_eq!(instruments.mongodb, None); @@ -119,10 +119,10 @@ mod tests { #[test] fn test_from_args_unknown_instrument_value() { - let args = AppArgs { + let args = RunArgs { instruments: vec!["unknown".into()], mongo_uri_env_name: Some("MONGODB_URI".into()), - ..AppArgs::test() + ..RunArgs::test() }; let instruments = Instruments::try_from(&args); assert!(instruments.is_err()); diff --git a/src/instruments/mongo_tracer.rs b/src/run/instruments/mongo_tracer.rs similarity index 99% rename from src/instruments/mongo_tracer.rs rename to src/run/instruments/mongo_tracer.rs index 50a5d400..9a6d4dc7 100644 --- a/src/instruments/mongo_tracer.rs +++ b/src/run/instruments/mongo_tracer.rs @@ -10,7 +10,8 @@ use reqwest::Client; use tokio::fs; use url::Url; -use crate::{helpers::get_env_variable, prelude::*}; +use crate::prelude::*; +use crate::run::helpers::get_env_variable; use super::MongoDBConfig; diff --git a/src/run/logger.rs b/src/run/logger.rs new file mode 100644 index 00000000..a0483378 --- /dev/null +++ b/src/run/logger.rs @@ -0,0 +1,39 @@ +use crate::logger::{GROUP_TARGET, OPENED_GROUP_TARGET}; +use crate::prelude::*; +use crate::run::{ci_provider::CIProvider, runner::RunData}; +use log::LevelFilter; +use simplelog::{CombinedLogger, WriteLogger}; +use std::fs::copy; +use std::path::PathBuf; +use tempfile::NamedTempFile; + +pub struct Logger { + log_file_path: PathBuf, +} + +impl Logger { + #[allow(clippy::borrowed_box)] + pub fn new(provider: &Box) -> Result { + let provider_logger = provider.get_logger(); + let log_file = NamedTempFile::new().context("Failed to create log file")?; + let log_file_path = log_file.path().to_path_buf(); + let file_logger_config = simplelog::ConfigBuilder::new() + // Groups are not logged to the file + .add_filter_ignore_str(GROUP_TARGET) + .add_filter_ignore_str(OPENED_GROUP_TARGET) + .build(); + let file_logger = WriteLogger::new(LevelFilter::Trace, file_logger_config, log_file); + CombinedLogger::init(vec![provider_logger, file_logger]) + .context("Failed to init logger")?; + Ok(Self { log_file_path }) + } + + pub fn persist_log_to_profile_folder(&self, run_data: &RunData) -> Result<()> { + let profile_folder = run_data.profile_folder.clone(); + let dest_log_file_path = profile_folder.join("runner.log"); + debug!("Persisting log file to {}", dest_log_file_path.display()); + log::logger().flush(); + copy(&self.log_file_path, dest_log_file_path).context("Failed to copy log file")?; + Ok(()) + } +} diff --git a/src/run/mod.rs b/src/run/mod.rs new file mode 100644 index 00000000..b1b03aaa --- /dev/null +++ b/src/run/mod.rs @@ -0,0 +1,130 @@ +use crate::api_client::CodSpeedAPIClient; +use crate::config::CodSpeedConfig; +use crate::prelude::*; +use crate::run::{config::Config, logger::Logger}; +use crate::VERSION; +use clap::Args; + +mod check_system; +pub mod ci_provider; +mod helpers; +mod instruments; +mod poll_results; +mod runner; +mod uploader; + +pub mod config; +pub mod logger; + +fn show_banner() { + let banner = format!( + r#" + ______ __ _____ __ + / ____/____ ____/ // ___/ ____ ___ ___ ____/ / + / / / __ \ / __ / \__ \ / __ \ / _ \ / _ \ / __ / +/ /___ / /_/ // /_/ / ___/ // /_/ // __// __// /_/ / +\____/ \____/ \__,_/ /____// .___/ \___/ \___/ \__,_/ + https://codspeed.io /_/ runner v{} +"#, + VERSION + ); + println!("{}", banner); + debug!("codspeed v{}", VERSION); +} + +#[derive(Args, Debug)] +pub struct RunArgs { + /// The upload URL to use for uploading the results, useful for on-premises installations + #[arg(long)] + pub upload_url: Option, + + /// The token to use for uploading the results, + #[arg(long, env = "CODSPEED_TOKEN")] + pub token: Option, + + /// The directory where the command will be executed. + #[arg(long)] + pub working_directory: Option, + + /// Comma-separated list of instruments to enable. Possible values: mongodb. + #[arg(long, value_delimiter = ',')] + pub instruments: Vec, + + /// The name of the environment variable that contains the MongoDB URI to patch. + /// If not provided, user will have to provide it dynamically through a CodSpeed integration. + /// + /// Only used if the `mongodb` instrument is enabled. + #[arg(long)] + pub mongo_uri_env_name: Option, + + /// Only for debugging purposes, skips the upload of the results + #[arg( + long, + default_value = "false", + hide = true, + env = "CODSPEED_SKIP_UPLOAD" + )] + pub skip_upload: bool, + + /// Only for debugging purposes, skips the setup of the runner + #[arg(long, default_value = "false", hide = true)] + pub skip_setup: bool, + + /// The bench command to run + pub command: Vec, +} + +#[cfg(test)] +impl RunArgs { + /// Constructs a new `RunArgs` with default values for testing purposes + pub fn test() -> Self { + Self { + upload_url: None, + token: None, + working_directory: None, + instruments: vec![], + mongo_uri_env_name: None, + skip_upload: false, + skip_setup: false, + command: vec![], + } + } +} + +pub async fn run(args: RunArgs, api_client: &CodSpeedAPIClient) -> Result<()> { + let mut config = Config::try_from(args)?; + let provider = ci_provider::get_provider(&config)?; + let codspeed_config = CodSpeedConfig::load()?; + let logger = Logger::new(&provider)?; + + if provider.get_provider_slug() != "local" { + show_banner(); + } + debug!("config: {:#?}", config); + + if provider.get_provider_slug() == "local" { + if codspeed_config.auth.token.is_none() { + bail!("You have to authenticate the CLI first. Run `codspeed auth login`."); + } + debug!("Using the token from the CodSpeed configuration file"); + config.set_token(codspeed_config.auth.token.clone()); + } + + let system_info = check_system::check_system()?; + let run_data = runner::run(&config, &system_info).await?; + + if !config.skip_upload { + start_group!("Uploading performance data"); + logger.persist_log_to_profile_folder(&run_data)?; + let upload_result = uploader::upload(&config, &system_info, &provider, &run_data).await?; + end_group!(); + + if provider.get_provider_slug() == "local" { + start_group!("Fetching the results"); + poll_results::poll_results(api_client, &provider, upload_result.run_id).await?; + end_group!(); + } + } + + Ok(()) +} diff --git a/src/run/poll_results.rs b/src/run/poll_results.rs new file mode 100644 index 00000000..cc979228 --- /dev/null +++ b/src/run/poll_results.rs @@ -0,0 +1,82 @@ +use std::time::Duration; + +use console::style; +use tokio::time::{sleep, Instant}; + +use crate::api_client::{ + CodSpeedAPIClient, FetchLocalRunReportResponse, FetchLocalRunReportVars, RunStatus, +}; +use crate::prelude::*; + +use super::ci_provider::CIProvider; + +const RUN_PROCESSING_MAX_DURATION: Duration = Duration::from_secs(60 * 5); // 5 minutes +const POLLING_INTERVAL: Duration = Duration::from_secs(1); + +#[allow(clippy::borrowed_box)] +pub async fn poll_results( + api_client: &CodSpeedAPIClient, + provider: &Box, + run_id: String, +) -> Result<()> { + let start = Instant::now(); + let provider_metadata = provider.get_provider_metadata()?; + let owner = provider_metadata.owner; + let name = provider_metadata.repository; + let fetch_local_run_report_vars = FetchLocalRunReportVars { + owner: owner.clone(), + name: name.clone(), + run_id: run_id.clone(), + }; + + let response; + loop { + if start.elapsed() > RUN_PROCESSING_MAX_DURATION { + bail!("Polling results timed out"); + } + + match api_client + .fetch_local_run_report(fetch_local_run_report_vars.clone()) + .await? + { + FetchLocalRunReportResponse { run, .. } if run.status != RunStatus::Completed => { + sleep(POLLING_INTERVAL).await; + } + response_from_api => { + response = response_from_api; + break; + } + } + } + + let report = response + .run + .head_reports + .into_iter() + .next() + .ok_or_else(|| anyhow!("No head report found in the run report"))?; + + if let Some(impact) = report.impact { + let rounded_impact = (impact * 100.0).round(); + let impact_text = if impact > 0.0 { + style(format!("+{}%", rounded_impact)).green().bold() + } else { + style(format!("{}%", rounded_impact)).red().bold() + }; + + info!( + "Impact: {} (allowed regression: -{}%)", + impact_text, + (response.allowed_regression * 100.0).round() + ); + } else { + info!("No impact detected, reason: {}", report.conclusion); + } + + info!( + "\nTo see the full report, visit: {}", + style(response.run.url).blue().bold().underlined() + ); + + Ok(()) +} diff --git a/src/runner/helpers/download_file.rs b/src/run/runner/helpers/download_file.rs similarity index 100% rename from src/runner/helpers/download_file.rs rename to src/run/runner/helpers/download_file.rs diff --git a/src/runner/helpers/ignored_objects_path.rs b/src/run/runner/helpers/ignored_objects_path.rs similarity index 100% rename from src/runner/helpers/ignored_objects_path.rs rename to src/run/runner/helpers/ignored_objects_path.rs diff --git a/src/runner/helpers/introspected_node/mod.rs b/src/run/runner/helpers/introspected_node/mod.rs similarity index 100% rename from src/runner/helpers/introspected_node/mod.rs rename to src/run/runner/helpers/introspected_node/mod.rs diff --git a/src/runner/helpers/introspected_node/node.sh b/src/run/runner/helpers/introspected_node/node.sh similarity index 100% rename from src/runner/helpers/introspected_node/node.sh rename to src/run/runner/helpers/introspected_node/node.sh diff --git a/src/runner/helpers/mod.rs b/src/run/runner/helpers/mod.rs similarity index 100% rename from src/runner/helpers/mod.rs rename to src/run/runner/helpers/mod.rs diff --git a/src/runner/helpers/perf_maps.rs b/src/run/runner/helpers/perf_maps.rs similarity index 100% rename from src/runner/helpers/perf_maps.rs rename to src/run/runner/helpers/perf_maps.rs diff --git a/src/runner/helpers/profile_folder.rs b/src/run/runner/helpers/profile_folder.rs similarity index 100% rename from src/runner/helpers/profile_folder.rs rename to src/run/runner/helpers/profile_folder.rs diff --git a/src/runner/mod.rs b/src/run/runner/mod.rs similarity index 88% rename from src/runner/mod.rs rename to src/run/runner/mod.rs index ecd94687..5ae8c8c4 100644 --- a/src/runner/mod.rs +++ b/src/run/runner/mod.rs @@ -1,4 +1,3 @@ -mod check_system; mod helpers; mod run; mod setup; diff --git a/src/runner/run.rs b/src/run/runner/run.rs similarity index 71% rename from src/runner/run.rs rename to src/run/runner/run.rs index 686d9570..46c78896 100644 --- a/src/runner/run.rs +++ b/src/run/runner/run.rs @@ -1,8 +1,11 @@ -use crate::{config::Config, instruments::mongo_tracer::MongoTracer, prelude::*}; +use crate::prelude::*; +use crate::run::{ + check_system::SystemInfo, config::Config, instruments::mongo_tracer::MongoTracer, +}; + use std::path::PathBuf; use super::{ - check_system::check_system, helpers::{perf_maps::harvest_perf_maps, profile_folder::create_profile_folder}, setup::setup, valgrind, @@ -12,15 +15,14 @@ pub struct RunData { pub profile_folder: PathBuf, } -pub async fn run(config: &Config) -> Result { +pub async fn run(config: &Config, system_info: &SystemInfo) -> Result { if !config.skip_setup { - start_group!("Prepare the environment"); - let system_info = check_system()?; - setup(&system_info, config).await?; + start_group!("Preparing the environment"); + setup(system_info, config).await?; end_group!(); } //TODO: add valgrind version check - start_opened_group!("Run the benchmarks"); + start_opened_group!("Running the benchmarks"); let profile_folder = create_profile_folder()?; let mongo_tracer = if let Some(mongodb_config) = &config.instruments.mongodb { let mut mongo_tracer = MongoTracer::try_from(&profile_folder, mongodb_config)?; diff --git a/src/runner/setup.rs b/src/run/runner/setup.rs similarity index 55% rename from src/runner/setup.rs rename to src/run/runner/setup.rs index 3c38d4f4..a039b47e 100644 --- a/src/runner/setup.rs +++ b/src/run/runner/setup.rs @@ -1,14 +1,13 @@ use std::{ - collections::HashMap, env, process::{Command, Stdio}, }; -use lazy_static::lazy_static; use url::Url; -use super::{check_system::SystemInfo, helpers::download_file::download_file}; -use crate::{config::Config, prelude::*, MONGODB_TRACER_VERSION, VALGRIND_CODSPEED_VERSION}; +use super::helpers::download_file::download_file; +use crate::run::{check_system::SystemInfo, config::Config}; +use crate::{prelude::*, MONGODB_TRACER_VERSION, VALGRIND_CODSPEED_VERSION}; /// Run a command with sudo if available fn run_with_sudo(command_args: &[&str]) -> Result<()> { @@ -39,65 +38,58 @@ fn run_with_sudo(command_args: &[&str]) -> Result<()> { Ok(()) } -lazy_static! { - static ref SYSTEM_INFO_TO_CODSPEED_VALGRIND_FILENAME: HashMap = { - let mut m = HashMap::new(); - m.insert( - SystemInfo { - os: "Ubuntu".to_string(), - os_version: "20.04".to_string(), - arch: "amd64".to_string(), - }, - format!( - "valgrind_{}_ubuntu-{}_amd64.deb", - VALGRIND_CODSPEED_VERSION, "20.04" - ), - ); - m.insert( - SystemInfo { - os: "Ubuntu".to_string(), - os_version: "22.04".to_string(), - arch: "amd64".to_string(), - }, - format!( - "valgrind_{}_ubuntu-{}_amd64.deb", - VALGRIND_CODSPEED_VERSION, "22.04" - ), - ); - m.insert( - SystemInfo { - os: "Debian".to_string(), - os_version: "11".to_string(), - arch: "amd64".to_string(), - }, - format!( - "valgrind_{}_ubuntu-{}_amd64.deb", - VALGRIND_CODSPEED_VERSION, "20.04" - ), - ); - m.insert( - SystemInfo { - os: "Debian".to_string(), - os_version: "12".to_string(), - arch: "amd64".to_string(), - }, - format!( - "valgrind_{}_ubuntu-{}_amd64.deb", - VALGRIND_CODSPEED_VERSION, "20.04" - ), - ); - m +fn get_codspeed_valgrind_filename(system_info: &SystemInfo) -> Result { + let version = match ( + system_info.os.as_str(), + system_info.os_version.as_str(), + system_info.arch.as_str(), + ) { + ("Ubuntu", "20.04", "x86_64") | ("Debian", "11", "x86_64") | ("Debian", "12", "x86_64") => { + "20.04" + } + ("Ubuntu", "22.04", "x86_64") => "22.04", + + _ => bail!("Unsupported system"), }; + + Ok(format!( + "valgrind_{}_ubuntu_{}_amd64.deb", + VALGRIND_CODSPEED_VERSION, version + )) +} + +fn is_valgrind_installed() -> bool { + let is_valgrind_installed = Command::new("which") + .arg("valgrind") + .output() + .is_ok_and(|output| output.status.success()); + if !is_valgrind_installed { + return false; + } + + if let Ok(version_output) = Command::new("valgrind").arg("--version").output() { + if !version_output.status.success() { + return false; + } + + let version = String::from_utf8_lossy(&version_output.stdout); + // TODO: use only VALGRIND_CODSPEED_VERSION here, the other value is when valgrind has been built locally + version.contains("valgrind-3.21.0.codspeed") || version.contains(VALGRIND_CODSPEED_VERSION) + } else { + false + } } async fn install_valgrind(system_info: &SystemInfo) -> Result<()> { + if is_valgrind_installed() { + debug!("Valgrind is already installed with the correct version, skipping installation"); + return Ok(()); + } debug!("Installing valgrind"); let valgrind_deb_url = format!( "https://github.com/CodSpeedHQ/valgrind-codspeed/releases/download/{}/{}", VALGRIND_CODSPEED_VERSION, - SYSTEM_INFO_TO_CODSPEED_VALGRIND_FILENAME - .get(system_info) - .context("Unsupported system")? + get_codspeed_valgrind_filename(system_info)? ); let deb_path = env::temp_dir().join("valgrind-codspeed.deb"); download_file(&Url::parse(valgrind_deb_url.as_str()).unwrap(), &deb_path).await?; @@ -150,18 +142,32 @@ mod tests { use super::*; #[test] - fn test_system_info_to_codspeed_valgrind_version() { + fn test_system_info_to_codspeed_valgrind_version_ubuntu() { + let system_info = SystemInfo { + os: "Ubuntu".to_string(), + os_version: "22.04".to_string(), + arch: "x86_64".to_string(), + host: "host".to_string(), + user: "user".to_string(), + }; + assert_eq!( + get_codspeed_valgrind_filename(&system_info).unwrap(), + "valgrind_3.21.0-0codspeed1_ubuntu_22.04_amd64.deb" + ); + } + + #[test] + fn test_system_info_to_codspeed_valgrind_version_debian() { let system_info = SystemInfo { os: "Debian".to_string(), os_version: "11".to_string(), - arch: "amd64".to_string(), + arch: "x86_64".to_string(), + host: "host".to_string(), + user: "user".to_string(), }; assert_eq!( - SYSTEM_INFO_TO_CODSPEED_VALGRIND_FILENAME[&system_info], - format!( - "valgrind_{}_ubuntu-{}_amd64.deb", - VALGRIND_CODSPEED_VERSION, "20.04" - ) + get_codspeed_valgrind_filename(&system_info).unwrap(), + "valgrind_3.21.0-0codspeed1_ubuntu_20.04_amd64.deb" ); } } diff --git a/src/runner/valgrind.rs b/src/run/runner/valgrind.rs similarity index 78% rename from src/runner/valgrind.rs rename to src/run/runner/valgrind.rs index 0877c80d..3c6d7ea9 100644 --- a/src/runner/valgrind.rs +++ b/src/run/runner/valgrind.rs @@ -1,8 +1,10 @@ -use crate::config::Config; -use crate::instruments::mongo_tracer::MongoTracer; +use crate::local_logger::suspend_progress_bar; use crate::prelude::*; -use crate::runner::helpers::ignored_objects_path::get_objects_path_to_ignore; -use crate::runner::helpers::introspected_node::setup_introspected_node; +use crate::run::{ + config::Config, instruments::mongo_tracer::MongoTracer, + runner::helpers::ignored_objects_path::get_objects_path_to_ignore, + runner::helpers::introspected_node::setup_introspected_node, +}; use lazy_static::lazy_static; use std::fs::canonicalize; use std::io::{Read, Write}; @@ -49,11 +51,16 @@ lazy_static! { }; } -fn get_bench_command(config: &Config) -> String { - let bench_command = &config.command; - bench_command +fn get_bench_command(config: &Config) -> Result { + let bench_command = &config.command.trim(); + + if bench_command.is_empty() { + bail!("The bench command is empty"); + } + + Ok(bench_command // Fixes a compatibility issue with cargo 1.66+ running directly under valgrind <3.20 - .replace("cargo codspeed", "cargo-codspeed") + .replace("cargo codspeed", "cargo-codspeed")) } pub const VALGRIND_EXECUTION_TARGET: &str = "valgrind::execution"; @@ -71,8 +78,10 @@ fn run_command_with_log_pipe(mut cmd: Command) -> Result { if bytes_read == 0 { break; } - writer.write_all(&buffer[..bytes_read])?; - trace!(target: VALGRIND_EXECUTION_TARGET, "{}{}", prefix, String::from_utf8_lossy(&buffer[..bytes_read])); + suspend_progress_bar(|| { + writer.write_all(&buffer[..bytes_read]).unwrap(); + trace!(target: VALGRIND_EXECUTION_TARGET, "{}{}", prefix, String::from_utf8_lossy(&buffer[..bytes_read])); + }); } Ok(()) } @@ -133,7 +142,7 @@ pub fn measure( .arg(format!("--log-file={}", log_path.to_str().unwrap()).as_str()); // Set the command to execute - cmd.args(["sh", "-c", get_bench_command(config).as_str()]); + cmd.args(["sh", "-c", get_bench_command(config)?.as_str()]); // TODO: refactor and move this to the `Instrumentation` trait if let Some(mongo_tracer) = mongo_tracer { @@ -154,13 +163,23 @@ pub fn measure( mod tests { use super::*; + #[test] + fn test_get_bench_command_empty() { + let config = Config::test(); + assert!(get_bench_command(&config).is_err()); + assert_eq!( + get_bench_command(&config).unwrap_err().to_string(), + "The bench command is empty" + ); + } + #[test] fn test_get_bench_command_cargo() { let config = Config { command: "cargo codspeed bench".into(), ..Config::test() }; - assert_eq!(get_bench_command(&config), "cargo-codspeed bench"); + assert_eq!(get_bench_command(&config).unwrap(), "cargo-codspeed bench"); } #[test] @@ -176,12 +195,10 @@ pytest tests/ --codspeed ..Config::test() }; assert_eq!( - get_bench_command(&config), - r#" -cargo-codspeed bench --features "foo bar" + get_bench_command(&config).unwrap(), + r#"cargo-codspeed bench --features "foo bar" pnpm vitest bench "my-app" -pytest tests/ --codspeed -"# +pytest tests/ --codspeed"# ); } } diff --git a/src/uploader/interfaces.rs b/src/run/uploader/interfaces.rs similarity index 82% rename from src/uploader/interfaces.rs rename to src/run/uploader/interfaces.rs index 3d3c1020..72423d8d 100644 --- a/src/uploader/interfaces.rs +++ b/src/run/uploader/interfaces.rs @@ -1,6 +1,9 @@ use serde::{Deserialize, Serialize}; -use crate::{ci_provider::interfaces::ProviderMetadata, instruments::InstrumentNames}; +use crate::run::{ + check_system::SystemInfo, ci_provider::interfaces::ProviderMetadata, + instruments::InstrumentNames, +}; #[derive(Deserialize, Serialize, Debug)] #[serde(rename_all = "camelCase")] @@ -21,6 +24,8 @@ pub struct Runner { pub name: String, pub version: String, pub instruments: Vec, + #[serde(flatten)] + pub system_info: SystemInfo, } #[derive(Deserialize, Serialize, Debug)] diff --git a/src/uploader/mod.rs b/src/run/uploader/mod.rs similarity index 100% rename from src/uploader/mod.rs rename to src/run/uploader/mod.rs diff --git a/src/uploader/snapshots/codspeed_runner__uploader__upload_metadata__tests__get_metadata_hash.snap b/src/run/uploader/snapshots/codspeed__run__uploader__upload_metadata__tests__get_metadata_hash.snap similarity index 80% rename from src/uploader/snapshots/codspeed_runner__uploader__upload_metadata__tests__get_metadata_hash.snap rename to src/run/uploader/snapshots/codspeed__run__uploader__upload_metadata__tests__get_metadata_hash.snap index 7d7e6f4a..118e8881 100644 --- a/src/uploader/snapshots/codspeed_runner__uploader__upload_metadata__tests__get_metadata_hash.snap +++ b/src/run/uploader/snapshots/codspeed__run__uploader__upload_metadata__tests__get_metadata_hash.snap @@ -1,9 +1,9 @@ --- -source: src/uploader/upload_metadata.rs +source: src/run/uploader/upload_metadata.rs expression: upload_metadata --- { - "version": 2, + "version": 3, "tokenless": true, "profileMd5": "jp/k05RKuqP3ERQuIIvx4Q==", "runner": { @@ -11,7 +11,12 @@ expression: upload_metadata "version": "2.1.0", "instruments": [ "MongoDB" - ] + ], + "os": "Ubuntu", + "osVersion": "20.04", + "arch": "x86_64", + "host": "host", + "user": "user" }, "platform": "github-actions", "commitHash": "5bd77cb0da72bef094893ed45fb793ff16ecfbe3", diff --git a/src/uploader/upload.rs b/src/run/uploader/upload.rs similarity index 70% rename from src/uploader/upload.rs rename to src/run/uploader/upload.rs index a76876fe..3cbaf780 100644 --- a/src/uploader/upload.rs +++ b/src/run/uploader/upload.rs @@ -1,9 +1,12 @@ -use crate::{ - ci_provider::CIProvider, config::Config, prelude::*, request_client::REQUEST_CLIENT, - runner::RunData, uploader::UploadError, +use crate::run::{ + check_system::SystemInfo, ci_provider::CIProvider, config::Config, runner::RunData, + uploader::UploadError, }; +use crate::{prelude::*, request_client::REQUEST_CLIENT}; use async_compression::tokio::write::GzipEncoder; use base64::{engine::general_purpose, Engine as _}; +use console::style; +use reqwest::StatusCode; use tokio::io::AsyncWriteExt; use tokio_tar::Builder; @@ -43,13 +46,23 @@ async fn retrieve_upload_data( if response.status().is_client_error() { let status = response.status(); let text = response.text().await?; - let error_message = serde_json::from_str::(&text) + let mut error_message = serde_json::from_str::(&text) .map(|body| body.error) .unwrap_or(text); + if status == StatusCode::UNAUTHORIZED { + let additional_message = if upload_metadata.platform == "local" { + "Run `codspeed auth login` to authenticate the CLI" + } else { + "Check that CODSPEED_TOKEN is set and has the correct value" + }; + error_message.push_str(&format!("\n\n{}", additional_message)); + } bail!( - "Failed to retrieve upload data: {}\n{}", + "Failed to retrieve upload data: {}\n -> {} {}", status, - error_message + style("Reason:").bold(), + // we have to manually apply the style to the error message, because nesting styles is not supported by the console crate: https://github.com/console-rs/console/issues/106 + style(error_message).red() ); } @@ -76,17 +89,31 @@ async fn upload_archive_buffer( Ok(()) } +pub struct UploadResult { + pub run_id: String, +} + +#[allow(clippy::borrowed_box)] pub async fn upload( config: &Config, - provider: Box, + system_info: &SystemInfo, + provider: &Box, run_data: &RunData, -) -> Result<()> { +) -> Result { let (archive_buffer, archive_hash) = get_profile_archive_buffer(run_data).await?; debug!("CI provider detected: {:#?}", provider.get_provider_name()); - let upload_metadata = provider.get_upload_metadata(config, &archive_hash)?; + let upload_metadata = provider.get_upload_metadata(config, system_info, &archive_hash)?; debug!("Upload metadata: {:#?}", upload_metadata); + info!( + "Linked repository: {}\n", + style(format!( + "{}/{}", + upload_metadata.provider_metadata.owner, upload_metadata.provider_metadata.repository + )) + .bold(), + ); if upload_metadata.tokenless { let hash = upload_metadata.get_hash(); info!("CodSpeed Run Hash: \"{}\"", hash); @@ -96,12 +123,14 @@ pub async fn upload( let upload_data = retrieve_upload_data(config, &upload_metadata).await?; debug!("runId: {}", upload_data.run_id); - info!("Uploading profile data..."); + info!("Uploading performance data..."); debug!("Uploading {} bytes...", archive_buffer.len()); upload_archive_buffer(&upload_data, archive_buffer, &archive_hash).await?; - info!("Results uploaded."); + info!("Performance data uploaded"); - Ok(()) + Ok(UploadResult { + run_id: upload_data.run_id, + }) } #[cfg(test)] @@ -110,7 +139,6 @@ mod tests { use url::Url; use super::*; - use crate::runner::RunData; use std::path::PathBuf; // TODO: remove the ignore when implementing network mocking @@ -129,6 +157,7 @@ mod tests { env!("CARGO_MANIFEST_DIR") )), }; + let system_info = SystemInfo::test(); async_with_vars( [ ("GITHUB_ACTIONS", Some("true")), @@ -158,8 +187,10 @@ mod tests { ("VERSION", Some("0.1.0")), ], async { - let provider = crate::ci_provider::get_provider(&config).unwrap(); - upload(&config, provider, &run_data).await.unwrap(); + let provider = crate::run::ci_provider::get_provider(&config).unwrap(); + upload(&config, &system_info, &provider, &run_data) + .await + .unwrap(); }, ) .await; diff --git a/src/uploader/upload_metadata.rs b/src/run/uploader/upload_metadata.rs similarity index 89% rename from src/uploader/upload_metadata.rs rename to src/run/uploader/upload_metadata.rs index 425f0d85..081bfa23 100644 --- a/src/uploader/upload_metadata.rs +++ b/src/run/uploader/upload_metadata.rs @@ -13,7 +13,8 @@ impl UploadMetadata { mod tests { use insta::assert_json_snapshot; - use crate::{ + use crate::run::{ + check_system::SystemInfo, ci_provider::interfaces::{GhData, ProviderMetadata, RunEvent, Sender}, instruments::InstrumentNames, uploader::{Runner, UploadMetadata}, @@ -22,13 +23,14 @@ mod tests { #[test] fn test_get_metadata_hash() { let upload_metadata = UploadMetadata { - version: Some(2), + version: Some(3), tokenless: true, profile_md5: "jp/k05RKuqP3ERQuIIvx4Q==".into(), runner: Runner { name: "codspeed-runner".into(), version: "2.1.0".into(), instruments: vec![InstrumentNames::MongoDB], + system_info: SystemInfo::test(), }, platform: "github-actions".into(), commit_hash: "5bd77cb0da72bef094893ed45fb793ff16ecfbe3".into(), @@ -54,7 +56,7 @@ mod tests { let hash = upload_metadata.get_hash(); assert_eq!( hash, - "8beb149c4645c666156e24fe0f68d24a63cec1d7756f35dd17cab1d84528ed7b" + "ada5057b0c440844a1558eed80a1993a41756984cc6147fdef459ce8a289f1d7" ); assert_json_snapshot!(upload_metadata); } diff --git a/src/runner/check_system.rs b/src/runner/check_system.rs deleted file mode 100644 index 5f5d0d74..00000000 --- a/src/runner/check_system.rs +++ /dev/null @@ -1,81 +0,0 @@ -use std::process::Command; - -use crate::prelude::*; - -/// Returns the OS and version of the system -/// -/// ## Example output -/// ``` -/// ("Ubuntu", "20.04") -/// ("Ubuntu", "22.04") -/// ("Debian", "11") -/// ("Debian", "12") -/// ``` -fn get_os_details() -> Result<(String, String)> { - let lsb_output = Command::new("lsb_release") - .args(["-i", "-r", "-s"]) - .output() - .map_err(|_| anyhow!("Failed to get system info"))?; - if !lsb_output.status.success() { - bail!("Failed to get system info"); - } - let output_str = - String::from_utf8(lsb_output.stdout).map_err(|_| anyhow!("Failed to parse system info"))?; - let mut lines = output_str.trim().lines(); - let os = lines - .next() - .ok_or_else(|| anyhow!("Failed to get OS info"))?; - let os_version = lines - .next() - .ok_or_else(|| anyhow!("Failed to get OS version"))?; - Ok((os.to_string(), os_version.to_string())) -} - -/// NOTE: Since this relies on `dpkg` this will only work on Debian based systems -fn get_arch() -> Result { - let arch_output = Command::new("dpkg") - .args(["--print-architecture"]) - .output() - .map_err(|_| anyhow!("Failed to get architecture info"))?; - if !arch_output.status.success() { - bail!("Failed to get architecture info"); - } - let output_str = String::from_utf8(arch_output.stdout) - .map_err(|_| anyhow!("Failed to parse architecture info"))?; - Ok(output_str.trim().to_string()) -} - -#[derive(Eq, PartialEq, Hash)] -pub struct SystemInfo { - pub os: String, - pub os_version: String, - pub arch: String, -} - -/// Checks if the system is supported -/// -/// Supported systems: -/// - Ubuntu 20.04 on amd64 -/// - Ubuntu 22.04 on amd64 -/// - Debian 11 on amd64 -/// - Debian 12 on amd64 -pub fn check_system() -> Result { - let (os, os_version) = get_os_details()?; - debug!("OS: {}, Version: {}", os, os_version); - match (os.as_str(), os_version.as_str()) { - ("Ubuntu", "20.04") | ("Ubuntu", "22.04") | ("Debian", "11") | ("Debian", "12") => (), - ("Ubuntu", _) => bail!("Only Ubuntu 20.04 and 22.04 are supported at the moment"), - ("Debian", _) => bail!("Only Debian 11 and 12 are supported at the moment"), - _ => bail!("Only Ubuntu and Debian are supported at the moment"), - } - let arch = get_arch()?; - debug!("Arch: {}", arch); - if arch != "amd64" { - bail!("Only amd64 is supported at the moment"); - } - Ok(SystemInfo { - os, - os_version, - arch, - }) -}