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)
[](https://github.com/CodSpeedHQ/runner/actions/workflows/ci.yml)
[](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,
- })
-}