├── .github └── workflows │ └── publish.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── README.md ├── lorikeet.svg └── src ├── graph.rs ├── junit.rs ├── lib.rs ├── main.rs ├── runner.rs ├── step ├── bash.rs ├── disk.rs ├── http.rs ├── mod.rs └── system.rs ├── submitter.rs └── yaml.rs /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: publish-lorikeet 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | 8 | jobs: 9 | build: 10 | runs-on: '${{ matrix.os }}' 11 | strategy: 12 | matrix: 13 | include: 14 | - os: macos-latest 15 | name: mac 16 | suffix: '' 17 | - os: ubuntu-latest 18 | suffix: '' 19 | name: linux 20 | #- os: windows-latest 21 | # suffix: .exe 22 | # name: windows 23 | steps: 24 | - uses: actions/checkout@v1 25 | - uses: actions/cache@v2 26 | with: 27 | path: | 28 | ~/.cargo/registry 29 | ~/.cargo/git 30 | target 31 | key: ${{ runner.os }}-cargo 32 | - uses: actions-rs/toolchain@v1 33 | with: 34 | toolchain: stable 35 | - uses: actions-rs/cargo@v1 36 | with: 37 | command: build 38 | args: --release 39 | - uses: svenstaro/upload-release-action@v2 40 | with: 41 | repo_token: ${{ secrets.GITHUB_TOKEN }} 42 | file: target/release/lorikeet${{ matrix.suffix }} 43 | asset_name: lorikeet-${{ matrix.name }}${{matrix.suffix}} 44 | tag: ${{ github.ref }} 45 | overwrite: true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | .idea/ 3 | /*.yml 4 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "0.7.18" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "ansi_term" 16 | version = "0.11.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" 19 | dependencies = [ 20 | "winapi", 21 | ] 22 | 23 | [[package]] 24 | name = "anyhow" 25 | version = "1.0.43" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "28ae2b3dec75a406790005a200b1bd89785afc02517a00ca99ecfe093ee9e6cf" 28 | 29 | [[package]] 30 | name = "atty" 31 | version = "0.2.14" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 34 | dependencies = [ 35 | "hermit-abi", 36 | "libc", 37 | "winapi", 38 | ] 39 | 40 | [[package]] 41 | name = "autocfg" 42 | version = "1.0.1" 43 | source = "registry+https://github.com/rust-lang/crates.io-index" 44 | checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" 45 | 46 | [[package]] 47 | name = "base-x" 48 | version = "0.2.8" 49 | source = "registry+https://github.com/rust-lang/crates.io-index" 50 | checksum = "a4521f3e3d031370679b3b140beb36dfe4801b09ac77e30c61941f97df3ef28b" 51 | 52 | [[package]] 53 | name = "base64" 54 | version = "0.13.0" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" 57 | 58 | [[package]] 59 | name = "bitflags" 60 | version = "1.3.2" 61 | source = "registry+https://github.com/rust-lang/crates.io-index" 62 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 63 | 64 | [[package]] 65 | name = "block-buffer" 66 | version = "0.7.3" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" 69 | dependencies = [ 70 | "block-padding", 71 | "byte-tools", 72 | "byteorder", 73 | "generic-array", 74 | ] 75 | 76 | [[package]] 77 | name = "block-padding" 78 | version = "0.1.5" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" 81 | dependencies = [ 82 | "byte-tools", 83 | ] 84 | 85 | [[package]] 86 | name = "bstr" 87 | version = "0.2.16" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "90682c8d613ad3373e66de8c6411e0ae2ab2571e879d2efbf73558cc66f21279" 90 | dependencies = [ 91 | "memchr", 92 | ] 93 | 94 | [[package]] 95 | name = "bumpalo" 96 | version = "3.7.0" 97 | source = "registry+https://github.com/rust-lang/crates.io-index" 98 | checksum = "9c59e7af012c713f529e7a3ee57ce9b31ddd858d4b512923602f74608b009631" 99 | 100 | [[package]] 101 | name = "byte-tools" 102 | version = "0.3.1" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" 105 | 106 | [[package]] 107 | name = "byteorder" 108 | version = "1.4.3" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" 111 | 112 | [[package]] 113 | name = "bytes" 114 | version = "1.1.0" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" 117 | 118 | [[package]] 119 | name = "cc" 120 | version = "1.0.70" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "d26a6ce4b6a484fa3edb70f7efa6fc430fd2b87285fe8b84304fd0936faa0dc0" 123 | 124 | [[package]] 125 | name = "cfg-if" 126 | version = "1.0.0" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 129 | 130 | [[package]] 131 | name = "chashmap" 132 | version = "2.2.2" 133 | source = "registry+https://github.com/rust-lang/crates.io-index" 134 | checksum = "ff41a3c2c1e39921b9003de14bf0439c7b63a9039637c291e1a64925d8ddfa45" 135 | dependencies = [ 136 | "owning_ref", 137 | "parking_lot 0.4.8", 138 | ] 139 | 140 | [[package]] 141 | name = "chrono" 142 | version = "0.4.19" 143 | source = "registry+https://github.com/rust-lang/crates.io-index" 144 | checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" 145 | dependencies = [ 146 | "libc", 147 | "num-integer", 148 | "num-traits", 149 | "serde", 150 | "time 0.1.44", 151 | "winapi", 152 | ] 153 | 154 | [[package]] 155 | name = "chrono-tz" 156 | version = "0.5.3" 157 | source = "registry+https://github.com/rust-lang/crates.io-index" 158 | checksum = "2554a3155fec064362507487171dcc4edc3df60cb10f3a1fb10ed8094822b120" 159 | dependencies = [ 160 | "chrono", 161 | "parse-zoneinfo", 162 | ] 163 | 164 | [[package]] 165 | name = "clap" 166 | version = "2.33.3" 167 | source = "registry+https://github.com/rust-lang/crates.io-index" 168 | checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" 169 | dependencies = [ 170 | "ansi_term", 171 | "atty", 172 | "bitflags", 173 | "strsim", 174 | "textwrap", 175 | "unicode-width", 176 | "vec_map", 177 | ] 178 | 179 | [[package]] 180 | name = "colored" 181 | version = "2.0.0" 182 | source = "registry+https://github.com/rust-lang/crates.io-index" 183 | checksum = "b3616f750b84d8f0de8a58bda93e08e2a81ad3f523089b05f1dffecab48c6cbd" 184 | dependencies = [ 185 | "atty", 186 | "lazy_static", 187 | "winapi", 188 | ] 189 | 190 | [[package]] 191 | name = "const_fn" 192 | version = "0.4.8" 193 | source = "registry+https://github.com/rust-lang/crates.io-index" 194 | checksum = "f92cfa0fd5690b3cf8c1ef2cabbd9b7ef22fa53cf5e1f92b05103f6d5d1cf6e7" 195 | 196 | [[package]] 197 | name = "cookie" 198 | version = "0.14.4" 199 | source = "registry+https://github.com/rust-lang/crates.io-index" 200 | checksum = "03a5d7b21829bc7b4bf4754a978a241ae54ea55a40f92bb20216e54096f4b951" 201 | dependencies = [ 202 | "time 0.2.27", 203 | "version_check", 204 | ] 205 | 206 | [[package]] 207 | name = "core-foundation" 208 | version = "0.9.1" 209 | source = "registry+https://github.com/rust-lang/crates.io-index" 210 | checksum = "0a89e2ae426ea83155dccf10c0fa6b1463ef6d5fcb44cee0b224a408fa640a62" 211 | dependencies = [ 212 | "core-foundation-sys", 213 | "libc", 214 | ] 215 | 216 | [[package]] 217 | name = "core-foundation-sys" 218 | version = "0.8.2" 219 | source = "registry+https://github.com/rust-lang/crates.io-index" 220 | checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b" 221 | 222 | [[package]] 223 | name = "crossbeam-utils" 224 | version = "0.8.5" 225 | source = "registry+https://github.com/rust-lang/crates.io-index" 226 | checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db" 227 | dependencies = [ 228 | "cfg-if", 229 | "lazy_static", 230 | ] 231 | 232 | [[package]] 233 | name = "deunicode" 234 | version = "0.4.3" 235 | source = "registry+https://github.com/rust-lang/crates.io-index" 236 | checksum = "850878694b7933ca4c9569d30a34b55031b9b139ee1fc7b94a527c4ef960d690" 237 | 238 | [[package]] 239 | name = "digest" 240 | version = "0.8.1" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" 243 | dependencies = [ 244 | "generic-array", 245 | ] 246 | 247 | [[package]] 248 | name = "discard" 249 | version = "1.0.4" 250 | source = "registry+https://github.com/rust-lang/crates.io-index" 251 | checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" 252 | 253 | [[package]] 254 | name = "dtoa" 255 | version = "0.4.8" 256 | source = "registry+https://github.com/rust-lang/crates.io-index" 257 | checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0" 258 | 259 | [[package]] 260 | name = "encoding_rs" 261 | version = "0.8.28" 262 | source = "registry+https://github.com/rust-lang/crates.io-index" 263 | checksum = "80df024fbc5ac80f87dfef0d9f5209a252f2a497f7f42944cff24d8253cac065" 264 | dependencies = [ 265 | "cfg-if", 266 | ] 267 | 268 | [[package]] 269 | name = "env_logger" 270 | version = "0.8.4" 271 | source = "registry+https://github.com/rust-lang/crates.io-index" 272 | checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3" 273 | dependencies = [ 274 | "atty", 275 | "humantime", 276 | "log", 277 | "regex", 278 | "termcolor", 279 | ] 280 | 281 | [[package]] 282 | name = "fake-simd" 283 | version = "0.1.2" 284 | source = "registry+https://github.com/rust-lang/crates.io-index" 285 | checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" 286 | 287 | [[package]] 288 | name = "fixedbitset" 289 | version = "0.2.0" 290 | source = "registry+https://github.com/rust-lang/crates.io-index" 291 | checksum = "37ab347416e802de484e4d03c7316c48f1ecb56574dfd4a46a80f173ce1de04d" 292 | 293 | [[package]] 294 | name = "fnv" 295 | version = "1.0.7" 296 | source = "registry+https://github.com/rust-lang/crates.io-index" 297 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 298 | 299 | [[package]] 300 | name = "foreign-types" 301 | version = "0.3.2" 302 | source = "registry+https://github.com/rust-lang/crates.io-index" 303 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 304 | dependencies = [ 305 | "foreign-types-shared", 306 | ] 307 | 308 | [[package]] 309 | name = "foreign-types-shared" 310 | version = "0.1.1" 311 | source = "registry+https://github.com/rust-lang/crates.io-index" 312 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 313 | 314 | [[package]] 315 | name = "form_urlencoded" 316 | version = "1.0.1" 317 | source = "registry+https://github.com/rust-lang/crates.io-index" 318 | checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" 319 | dependencies = [ 320 | "matches", 321 | "percent-encoding", 322 | ] 323 | 324 | [[package]] 325 | name = "fuchsia-cprng" 326 | version = "0.1.1" 327 | source = "registry+https://github.com/rust-lang/crates.io-index" 328 | checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" 329 | 330 | [[package]] 331 | name = "futures" 332 | version = "0.3.17" 333 | source = "registry+https://github.com/rust-lang/crates.io-index" 334 | checksum = "a12aa0eb539080d55c3f2d45a67c3b58b6b0773c1a3ca2dfec66d58c97fd66ca" 335 | dependencies = [ 336 | "futures-channel", 337 | "futures-core", 338 | "futures-executor", 339 | "futures-io", 340 | "futures-sink", 341 | "futures-task", 342 | "futures-util", 343 | ] 344 | 345 | [[package]] 346 | name = "futures-channel" 347 | version = "0.3.17" 348 | source = "registry+https://github.com/rust-lang/crates.io-index" 349 | checksum = "5da6ba8c3bb3c165d3c7319fc1cc8304facf1fb8db99c5de877183c08a273888" 350 | dependencies = [ 351 | "futures-core", 352 | "futures-sink", 353 | ] 354 | 355 | [[package]] 356 | name = "futures-core" 357 | version = "0.3.17" 358 | source = "registry+https://github.com/rust-lang/crates.io-index" 359 | checksum = "88d1c26957f23603395cd326b0ffe64124b818f4449552f960d815cfba83a53d" 360 | 361 | [[package]] 362 | name = "futures-executor" 363 | version = "0.3.17" 364 | source = "registry+https://github.com/rust-lang/crates.io-index" 365 | checksum = "45025be030969d763025784f7f355043dc6bc74093e4ecc5000ca4dc50d8745c" 366 | dependencies = [ 367 | "futures-core", 368 | "futures-task", 369 | "futures-util", 370 | ] 371 | 372 | [[package]] 373 | name = "futures-io" 374 | version = "0.3.17" 375 | source = "registry+https://github.com/rust-lang/crates.io-index" 376 | checksum = "522de2a0fe3e380f1bc577ba0474108faf3f6b18321dbf60b3b9c39a75073377" 377 | 378 | [[package]] 379 | name = "futures-macro" 380 | version = "0.3.17" 381 | source = "registry+https://github.com/rust-lang/crates.io-index" 382 | checksum = "18e4a4b95cea4b4ccbcf1c5675ca7c4ee4e9e75eb79944d07defde18068f79bb" 383 | dependencies = [ 384 | "autocfg", 385 | "proc-macro-hack", 386 | "proc-macro2", 387 | "quote", 388 | "syn", 389 | ] 390 | 391 | [[package]] 392 | name = "futures-sink" 393 | version = "0.3.17" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "36ea153c13024fe480590b3e3d4cad89a0cfacecc24577b68f86c6ced9c2bc11" 396 | 397 | [[package]] 398 | name = "futures-task" 399 | version = "0.3.17" 400 | source = "registry+https://github.com/rust-lang/crates.io-index" 401 | checksum = "1d3d00f4eddb73e498a54394f228cd55853bdf059259e8e7bc6e69d408892e99" 402 | 403 | [[package]] 404 | name = "futures-util" 405 | version = "0.3.17" 406 | source = "registry+https://github.com/rust-lang/crates.io-index" 407 | checksum = "36568465210a3a6ee45e1f165136d68671471a501e632e9a98d96872222b5481" 408 | dependencies = [ 409 | "autocfg", 410 | "futures-channel", 411 | "futures-core", 412 | "futures-io", 413 | "futures-macro", 414 | "futures-sink", 415 | "futures-task", 416 | "memchr", 417 | "pin-project-lite", 418 | "pin-utils", 419 | "proc-macro-hack", 420 | "proc-macro-nested", 421 | "slab", 422 | ] 423 | 424 | [[package]] 425 | name = "generic-array" 426 | version = "0.12.4" 427 | source = "registry+https://github.com/rust-lang/crates.io-index" 428 | checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" 429 | dependencies = [ 430 | "typenum", 431 | ] 432 | 433 | [[package]] 434 | name = "getrandom" 435 | version = "0.2.3" 436 | source = "registry+https://github.com/rust-lang/crates.io-index" 437 | checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" 438 | dependencies = [ 439 | "cfg-if", 440 | "libc", 441 | "wasi", 442 | ] 443 | 444 | [[package]] 445 | name = "globset" 446 | version = "0.4.8" 447 | source = "registry+https://github.com/rust-lang/crates.io-index" 448 | checksum = "10463d9ff00a2a068db14231982f5132edebad0d7660cd956a1c30292dbcbfbd" 449 | dependencies = [ 450 | "aho-corasick", 451 | "bstr", 452 | "fnv", 453 | "log", 454 | "regex", 455 | ] 456 | 457 | [[package]] 458 | name = "globwalk" 459 | version = "0.8.1" 460 | source = "registry+https://github.com/rust-lang/crates.io-index" 461 | checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc" 462 | dependencies = [ 463 | "bitflags", 464 | "ignore", 465 | "walkdir", 466 | ] 467 | 468 | [[package]] 469 | name = "h2" 470 | version = "0.3.4" 471 | source = "registry+https://github.com/rust-lang/crates.io-index" 472 | checksum = "d7f3675cfef6a30c8031cf9e6493ebdc3bb3272a3fea3923c4210d1830e6a472" 473 | dependencies = [ 474 | "bytes", 475 | "fnv", 476 | "futures-core", 477 | "futures-sink", 478 | "futures-util", 479 | "http", 480 | "indexmap", 481 | "slab", 482 | "tokio", 483 | "tokio-util", 484 | "tracing", 485 | ] 486 | 487 | [[package]] 488 | name = "hashbrown" 489 | version = "0.11.2" 490 | source = "registry+https://github.com/rust-lang/crates.io-index" 491 | checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" 492 | 493 | [[package]] 494 | name = "heck" 495 | version = "0.3.3" 496 | source = "registry+https://github.com/rust-lang/crates.io-index" 497 | checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" 498 | dependencies = [ 499 | "unicode-segmentation", 500 | ] 501 | 502 | [[package]] 503 | name = "hermit-abi" 504 | version = "0.1.19" 505 | source = "registry+https://github.com/rust-lang/crates.io-index" 506 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 507 | dependencies = [ 508 | "libc", 509 | ] 510 | 511 | [[package]] 512 | name = "hostname" 513 | version = "0.3.1" 514 | source = "registry+https://github.com/rust-lang/crates.io-index" 515 | checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" 516 | dependencies = [ 517 | "libc", 518 | "match_cfg", 519 | "winapi", 520 | ] 521 | 522 | [[package]] 523 | name = "http" 524 | version = "0.2.4" 525 | source = "registry+https://github.com/rust-lang/crates.io-index" 526 | checksum = "527e8c9ac747e28542699a951517aa9a6945af506cd1f2e1b53a576c17b6cc11" 527 | dependencies = [ 528 | "bytes", 529 | "fnv", 530 | "itoa", 531 | ] 532 | 533 | [[package]] 534 | name = "http-body" 535 | version = "0.4.3" 536 | source = "registry+https://github.com/rust-lang/crates.io-index" 537 | checksum = "399c583b2979440c60be0821a6199eca73bc3c8dcd9d070d75ac726e2c6186e5" 538 | dependencies = [ 539 | "bytes", 540 | "http", 541 | "pin-project-lite", 542 | ] 543 | 544 | [[package]] 545 | name = "httparse" 546 | version = "1.5.1" 547 | source = "registry+https://github.com/rust-lang/crates.io-index" 548 | checksum = "acd94fdbe1d4ff688b67b04eee2e17bd50995534a61539e45adfefb45e5e5503" 549 | 550 | [[package]] 551 | name = "httpdate" 552 | version = "1.0.1" 553 | source = "registry+https://github.com/rust-lang/crates.io-index" 554 | checksum = "6456b8a6c8f33fee7d958fcd1b60d55b11940a79e63ae87013e6d22e26034440" 555 | 556 | [[package]] 557 | name = "humansize" 558 | version = "1.1.1" 559 | source = "registry+https://github.com/rust-lang/crates.io-index" 560 | checksum = "02296996cb8796d7c6e3bc2d9211b7802812d36999a51bb754123ead7d37d026" 561 | 562 | [[package]] 563 | name = "humantime" 564 | version = "2.1.0" 565 | source = "registry+https://github.com/rust-lang/crates.io-index" 566 | checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" 567 | 568 | [[package]] 569 | name = "hyper" 570 | version = "0.14.12" 571 | source = "registry+https://github.com/rust-lang/crates.io-index" 572 | checksum = "13f67199e765030fa08fe0bd581af683f0d5bc04ea09c2b1102012c5fb90e7fd" 573 | dependencies = [ 574 | "bytes", 575 | "futures-channel", 576 | "futures-core", 577 | "futures-util", 578 | "h2", 579 | "http", 580 | "http-body", 581 | "httparse", 582 | "httpdate", 583 | "itoa", 584 | "pin-project-lite", 585 | "socket2", 586 | "tokio", 587 | "tower-service", 588 | "tracing", 589 | "want", 590 | ] 591 | 592 | [[package]] 593 | name = "hyper-tls" 594 | version = "0.5.0" 595 | source = "registry+https://github.com/rust-lang/crates.io-index" 596 | checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" 597 | dependencies = [ 598 | "bytes", 599 | "hyper", 600 | "native-tls", 601 | "tokio", 602 | "tokio-native-tls", 603 | ] 604 | 605 | [[package]] 606 | name = "idna" 607 | version = "0.2.3" 608 | source = "registry+https://github.com/rust-lang/crates.io-index" 609 | checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" 610 | dependencies = [ 611 | "matches", 612 | "unicode-bidi", 613 | "unicode-normalization", 614 | ] 615 | 616 | [[package]] 617 | name = "ignore" 618 | version = "0.4.18" 619 | source = "registry+https://github.com/rust-lang/crates.io-index" 620 | checksum = "713f1b139373f96a2e0ce3ac931cd01ee973c3c5dd7c40c0c2efe96ad2b6751d" 621 | dependencies = [ 622 | "crossbeam-utils", 623 | "globset", 624 | "lazy_static", 625 | "log", 626 | "memchr", 627 | "regex", 628 | "same-file", 629 | "thread_local", 630 | "walkdir", 631 | "winapi-util", 632 | ] 633 | 634 | [[package]] 635 | name = "indexmap" 636 | version = "1.7.0" 637 | source = "registry+https://github.com/rust-lang/crates.io-index" 638 | checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" 639 | dependencies = [ 640 | "autocfg", 641 | "hashbrown", 642 | ] 643 | 644 | [[package]] 645 | name = "instant" 646 | version = "0.1.10" 647 | source = "registry+https://github.com/rust-lang/crates.io-index" 648 | checksum = "bee0328b1209d157ef001c94dd85b4f8f64139adb0eac2659f4b08382b2f474d" 649 | dependencies = [ 650 | "cfg-if", 651 | ] 652 | 653 | [[package]] 654 | name = "ipnet" 655 | version = "2.3.1" 656 | source = "registry+https://github.com/rust-lang/crates.io-index" 657 | checksum = "68f2d64f2edebec4ce84ad108148e67e1064789bee435edc5b60ad398714a3a9" 658 | 659 | [[package]] 660 | name = "itoa" 661 | version = "0.4.8" 662 | source = "registry+https://github.com/rust-lang/crates.io-index" 663 | checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" 664 | 665 | [[package]] 666 | name = "jmespath" 667 | version = "0.3.0" 668 | source = "registry+https://github.com/rust-lang/crates.io-index" 669 | checksum = "017f8f53dd3b8ada762acb1f850da2a742d0ef3f921c60849a644380de1d683a" 670 | dependencies = [ 671 | "lazy_static", 672 | "serde", 673 | "serde_json", 674 | "slug", 675 | ] 676 | 677 | [[package]] 678 | name = "js-sys" 679 | version = "0.3.53" 680 | source = "registry+https://github.com/rust-lang/crates.io-index" 681 | checksum = "e4bf49d50e2961077d9c99f4b7997d770a1114f087c3c2e0069b36c13fc2979d" 682 | dependencies = [ 683 | "wasm-bindgen", 684 | ] 685 | 686 | [[package]] 687 | name = "lazy_static" 688 | version = "1.4.0" 689 | source = "registry+https://github.com/rust-lang/crates.io-index" 690 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 691 | 692 | [[package]] 693 | name = "libc" 694 | version = "0.2.101" 695 | source = "registry+https://github.com/rust-lang/crates.io-index" 696 | checksum = "3cb00336871be5ed2c8ed44b60ae9959dc5b9f08539422ed43f09e34ecaeba21" 697 | 698 | [[package]] 699 | name = "linked-hash-map" 700 | version = "0.5.4" 701 | source = "registry+https://github.com/rust-lang/crates.io-index" 702 | checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" 703 | dependencies = [ 704 | "serde", 705 | "serde_test", 706 | ] 707 | 708 | [[package]] 709 | name = "lock_api" 710 | version = "0.4.5" 711 | source = "registry+https://github.com/rust-lang/crates.io-index" 712 | checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109" 713 | dependencies = [ 714 | "scopeguard", 715 | ] 716 | 717 | [[package]] 718 | name = "log" 719 | version = "0.4.14" 720 | source = "registry+https://github.com/rust-lang/crates.io-index" 721 | checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" 722 | dependencies = [ 723 | "cfg-if", 724 | ] 725 | 726 | [[package]] 727 | name = "lorikeet" 728 | version = "0.15.0" 729 | dependencies = [ 730 | "anyhow", 731 | "atty", 732 | "chashmap", 733 | "chrono", 734 | "colored", 735 | "cookie", 736 | "env_logger", 737 | "futures", 738 | "hostname", 739 | "jmespath", 740 | "lazy_static", 741 | "libc", 742 | "linked-hash-map", 743 | "log", 744 | "openssl-probe", 745 | "petgraph", 746 | "quick-xml", 747 | "regex", 748 | "reqwest", 749 | "serde", 750 | "serde_json", 751 | "serde_yaml", 752 | "structopt", 753 | "sys-info", 754 | "tera", 755 | "tokio", 756 | "tokio-util", 757 | ] 758 | 759 | [[package]] 760 | name = "maplit" 761 | version = "1.0.2" 762 | source = "registry+https://github.com/rust-lang/crates.io-index" 763 | checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" 764 | 765 | [[package]] 766 | name = "match_cfg" 767 | version = "0.1.0" 768 | source = "registry+https://github.com/rust-lang/crates.io-index" 769 | checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" 770 | 771 | [[package]] 772 | name = "matches" 773 | version = "0.1.9" 774 | source = "registry+https://github.com/rust-lang/crates.io-index" 775 | checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" 776 | 777 | [[package]] 778 | name = "maybe-uninit" 779 | version = "2.0.0" 780 | source = "registry+https://github.com/rust-lang/crates.io-index" 781 | checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" 782 | 783 | [[package]] 784 | name = "memchr" 785 | version = "2.4.1" 786 | source = "registry+https://github.com/rust-lang/crates.io-index" 787 | checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" 788 | 789 | [[package]] 790 | name = "mime" 791 | version = "0.3.16" 792 | source = "registry+https://github.com/rust-lang/crates.io-index" 793 | checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" 794 | 795 | [[package]] 796 | name = "mime_guess" 797 | version = "2.0.3" 798 | source = "registry+https://github.com/rust-lang/crates.io-index" 799 | checksum = "2684d4c2e97d99848d30b324b00c8fcc7e5c897b7cbb5819b09e7c90e8baf212" 800 | dependencies = [ 801 | "mime", 802 | "unicase", 803 | ] 804 | 805 | [[package]] 806 | name = "mio" 807 | version = "0.7.13" 808 | source = "registry+https://github.com/rust-lang/crates.io-index" 809 | checksum = "8c2bdb6314ec10835cd3293dd268473a835c02b7b352e788be788b3c6ca6bb16" 810 | dependencies = [ 811 | "libc", 812 | "log", 813 | "miow", 814 | "ntapi", 815 | "winapi", 816 | ] 817 | 818 | [[package]] 819 | name = "miow" 820 | version = "0.3.7" 821 | source = "registry+https://github.com/rust-lang/crates.io-index" 822 | checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" 823 | dependencies = [ 824 | "winapi", 825 | ] 826 | 827 | [[package]] 828 | name = "native-tls" 829 | version = "0.2.8" 830 | source = "registry+https://github.com/rust-lang/crates.io-index" 831 | checksum = "48ba9f7719b5a0f42f338907614285fb5fd70e53858141f69898a1fb7203b24d" 832 | dependencies = [ 833 | "lazy_static", 834 | "libc", 835 | "log", 836 | "openssl", 837 | "openssl-probe", 838 | "openssl-sys", 839 | "schannel", 840 | "security-framework", 841 | "security-framework-sys", 842 | "tempfile", 843 | ] 844 | 845 | [[package]] 846 | name = "ntapi" 847 | version = "0.3.6" 848 | source = "registry+https://github.com/rust-lang/crates.io-index" 849 | checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" 850 | dependencies = [ 851 | "winapi", 852 | ] 853 | 854 | [[package]] 855 | name = "num-integer" 856 | version = "0.1.44" 857 | source = "registry+https://github.com/rust-lang/crates.io-index" 858 | checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" 859 | dependencies = [ 860 | "autocfg", 861 | "num-traits", 862 | ] 863 | 864 | [[package]] 865 | name = "num-traits" 866 | version = "0.2.14" 867 | source = "registry+https://github.com/rust-lang/crates.io-index" 868 | checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" 869 | dependencies = [ 870 | "autocfg", 871 | ] 872 | 873 | [[package]] 874 | name = "num_cpus" 875 | version = "1.13.0" 876 | source = "registry+https://github.com/rust-lang/crates.io-index" 877 | checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" 878 | dependencies = [ 879 | "hermit-abi", 880 | "libc", 881 | ] 882 | 883 | [[package]] 884 | name = "once_cell" 885 | version = "1.8.0" 886 | source = "registry+https://github.com/rust-lang/crates.io-index" 887 | checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" 888 | 889 | [[package]] 890 | name = "opaque-debug" 891 | version = "0.2.3" 892 | source = "registry+https://github.com/rust-lang/crates.io-index" 893 | checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" 894 | 895 | [[package]] 896 | name = "openssl" 897 | version = "0.10.36" 898 | source = "registry+https://github.com/rust-lang/crates.io-index" 899 | checksum = "8d9facdb76fec0b73c406f125d44d86fdad818d66fef0531eec9233ca425ff4a" 900 | dependencies = [ 901 | "bitflags", 902 | "cfg-if", 903 | "foreign-types", 904 | "libc", 905 | "once_cell", 906 | "openssl-sys", 907 | ] 908 | 909 | [[package]] 910 | name = "openssl-probe" 911 | version = "0.1.4" 912 | source = "registry+https://github.com/rust-lang/crates.io-index" 913 | checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a" 914 | 915 | [[package]] 916 | name = "openssl-sys" 917 | version = "0.9.66" 918 | source = "registry+https://github.com/rust-lang/crates.io-index" 919 | checksum = "1996d2d305e561b70d1ee0c53f1542833f4e1ac6ce9a6708b6ff2738ca67dc82" 920 | dependencies = [ 921 | "autocfg", 922 | "cc", 923 | "libc", 924 | "pkg-config", 925 | "vcpkg", 926 | ] 927 | 928 | [[package]] 929 | name = "owning_ref" 930 | version = "0.3.3" 931 | source = "registry+https://github.com/rust-lang/crates.io-index" 932 | checksum = "cdf84f41639e037b484f93433aa3897863b561ed65c6e59c7073d7c561710f37" 933 | dependencies = [ 934 | "stable_deref_trait", 935 | ] 936 | 937 | [[package]] 938 | name = "parking_lot" 939 | version = "0.4.8" 940 | source = "registry+https://github.com/rust-lang/crates.io-index" 941 | checksum = "149d8f5b97f3c1133e3cfcd8886449959e856b557ff281e292b733d7c69e005e" 942 | dependencies = [ 943 | "owning_ref", 944 | "parking_lot_core 0.2.14", 945 | ] 946 | 947 | [[package]] 948 | name = "parking_lot" 949 | version = "0.11.2" 950 | source = "registry+https://github.com/rust-lang/crates.io-index" 951 | checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" 952 | dependencies = [ 953 | "instant", 954 | "lock_api", 955 | "parking_lot_core 0.8.5", 956 | ] 957 | 958 | [[package]] 959 | name = "parking_lot_core" 960 | version = "0.2.14" 961 | source = "registry+https://github.com/rust-lang/crates.io-index" 962 | checksum = "4db1a8ccf734a7bce794cc19b3df06ed87ab2f3907036b693c68f56b4d4537fa" 963 | dependencies = [ 964 | "libc", 965 | "rand 0.4.6", 966 | "smallvec 0.6.14", 967 | "winapi", 968 | ] 969 | 970 | [[package]] 971 | name = "parking_lot_core" 972 | version = "0.8.5" 973 | source = "registry+https://github.com/rust-lang/crates.io-index" 974 | checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" 975 | dependencies = [ 976 | "cfg-if", 977 | "instant", 978 | "libc", 979 | "redox_syscall", 980 | "smallvec 1.6.1", 981 | "winapi", 982 | ] 983 | 984 | [[package]] 985 | name = "parse-zoneinfo" 986 | version = "0.3.0" 987 | source = "registry+https://github.com/rust-lang/crates.io-index" 988 | checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41" 989 | dependencies = [ 990 | "regex", 991 | ] 992 | 993 | [[package]] 994 | name = "percent-encoding" 995 | version = "2.1.0" 996 | source = "registry+https://github.com/rust-lang/crates.io-index" 997 | checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" 998 | 999 | [[package]] 1000 | name = "pest" 1001 | version = "2.1.3" 1002 | source = "registry+https://github.com/rust-lang/crates.io-index" 1003 | checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53" 1004 | dependencies = [ 1005 | "ucd-trie", 1006 | ] 1007 | 1008 | [[package]] 1009 | name = "pest_derive" 1010 | version = "2.1.0" 1011 | source = "registry+https://github.com/rust-lang/crates.io-index" 1012 | checksum = "833d1ae558dc601e9a60366421196a8d94bc0ac980476d0b67e1d0988d72b2d0" 1013 | dependencies = [ 1014 | "pest", 1015 | "pest_generator", 1016 | ] 1017 | 1018 | [[package]] 1019 | name = "pest_generator" 1020 | version = "2.1.3" 1021 | source = "registry+https://github.com/rust-lang/crates.io-index" 1022 | checksum = "99b8db626e31e5b81787b9783425769681b347011cc59471e33ea46d2ea0cf55" 1023 | dependencies = [ 1024 | "pest", 1025 | "pest_meta", 1026 | "proc-macro2", 1027 | "quote", 1028 | "syn", 1029 | ] 1030 | 1031 | [[package]] 1032 | name = "pest_meta" 1033 | version = "2.1.3" 1034 | source = "registry+https://github.com/rust-lang/crates.io-index" 1035 | checksum = "54be6e404f5317079812fc8f9f5279de376d8856929e21c184ecf6bbd692a11d" 1036 | dependencies = [ 1037 | "maplit", 1038 | "pest", 1039 | "sha-1", 1040 | ] 1041 | 1042 | [[package]] 1043 | name = "petgraph" 1044 | version = "0.5.1" 1045 | source = "registry+https://github.com/rust-lang/crates.io-index" 1046 | checksum = "467d164a6de56270bd7c4d070df81d07beace25012d5103ced4e9ff08d6afdb7" 1047 | dependencies = [ 1048 | "fixedbitset", 1049 | "indexmap", 1050 | ] 1051 | 1052 | [[package]] 1053 | name = "pin-project-lite" 1054 | version = "0.2.7" 1055 | source = "registry+https://github.com/rust-lang/crates.io-index" 1056 | checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443" 1057 | 1058 | [[package]] 1059 | name = "pin-utils" 1060 | version = "0.1.0" 1061 | source = "registry+https://github.com/rust-lang/crates.io-index" 1062 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 1063 | 1064 | [[package]] 1065 | name = "pkg-config" 1066 | version = "0.3.19" 1067 | source = "registry+https://github.com/rust-lang/crates.io-index" 1068 | checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" 1069 | 1070 | [[package]] 1071 | name = "ppv-lite86" 1072 | version = "0.2.10" 1073 | source = "registry+https://github.com/rust-lang/crates.io-index" 1074 | checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" 1075 | 1076 | [[package]] 1077 | name = "proc-macro-error" 1078 | version = "1.0.4" 1079 | source = "registry+https://github.com/rust-lang/crates.io-index" 1080 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 1081 | dependencies = [ 1082 | "proc-macro-error-attr", 1083 | "proc-macro2", 1084 | "quote", 1085 | "syn", 1086 | "version_check", 1087 | ] 1088 | 1089 | [[package]] 1090 | name = "proc-macro-error-attr" 1091 | version = "1.0.4" 1092 | source = "registry+https://github.com/rust-lang/crates.io-index" 1093 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 1094 | dependencies = [ 1095 | "proc-macro2", 1096 | "quote", 1097 | "version_check", 1098 | ] 1099 | 1100 | [[package]] 1101 | name = "proc-macro-hack" 1102 | version = "0.5.19" 1103 | source = "registry+https://github.com/rust-lang/crates.io-index" 1104 | checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" 1105 | 1106 | [[package]] 1107 | name = "proc-macro-nested" 1108 | version = "0.1.7" 1109 | source = "registry+https://github.com/rust-lang/crates.io-index" 1110 | checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086" 1111 | 1112 | [[package]] 1113 | name = "proc-macro2" 1114 | version = "1.0.29" 1115 | source = "registry+https://github.com/rust-lang/crates.io-index" 1116 | checksum = "b9f5105d4fdaab20335ca9565e106a5d9b82b6219b5ba735731124ac6711d23d" 1117 | dependencies = [ 1118 | "unicode-xid", 1119 | ] 1120 | 1121 | [[package]] 1122 | name = "quick-xml" 1123 | version = "0.21.0" 1124 | source = "registry+https://github.com/rust-lang/crates.io-index" 1125 | checksum = "0452695941410a58c8ce4391707ba9bad26a247173bd9886a05a5e8a8babec75" 1126 | dependencies = [ 1127 | "memchr", 1128 | ] 1129 | 1130 | [[package]] 1131 | name = "quote" 1132 | version = "1.0.9" 1133 | source = "registry+https://github.com/rust-lang/crates.io-index" 1134 | checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" 1135 | dependencies = [ 1136 | "proc-macro2", 1137 | ] 1138 | 1139 | [[package]] 1140 | name = "rand" 1141 | version = "0.4.6" 1142 | source = "registry+https://github.com/rust-lang/crates.io-index" 1143 | checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" 1144 | dependencies = [ 1145 | "fuchsia-cprng", 1146 | "libc", 1147 | "rand_core 0.3.1", 1148 | "rdrand", 1149 | "winapi", 1150 | ] 1151 | 1152 | [[package]] 1153 | name = "rand" 1154 | version = "0.8.4" 1155 | source = "registry+https://github.com/rust-lang/crates.io-index" 1156 | checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" 1157 | dependencies = [ 1158 | "libc", 1159 | "rand_chacha", 1160 | "rand_core 0.6.3", 1161 | "rand_hc", 1162 | ] 1163 | 1164 | [[package]] 1165 | name = "rand_chacha" 1166 | version = "0.3.1" 1167 | source = "registry+https://github.com/rust-lang/crates.io-index" 1168 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 1169 | dependencies = [ 1170 | "ppv-lite86", 1171 | "rand_core 0.6.3", 1172 | ] 1173 | 1174 | [[package]] 1175 | name = "rand_core" 1176 | version = "0.3.1" 1177 | source = "registry+https://github.com/rust-lang/crates.io-index" 1178 | checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" 1179 | dependencies = [ 1180 | "rand_core 0.4.2", 1181 | ] 1182 | 1183 | [[package]] 1184 | name = "rand_core" 1185 | version = "0.4.2" 1186 | source = "registry+https://github.com/rust-lang/crates.io-index" 1187 | checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" 1188 | 1189 | [[package]] 1190 | name = "rand_core" 1191 | version = "0.6.3" 1192 | source = "registry+https://github.com/rust-lang/crates.io-index" 1193 | checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" 1194 | dependencies = [ 1195 | "getrandom", 1196 | ] 1197 | 1198 | [[package]] 1199 | name = "rand_hc" 1200 | version = "0.3.1" 1201 | source = "registry+https://github.com/rust-lang/crates.io-index" 1202 | checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" 1203 | dependencies = [ 1204 | "rand_core 0.6.3", 1205 | ] 1206 | 1207 | [[package]] 1208 | name = "rdrand" 1209 | version = "0.4.0" 1210 | source = "registry+https://github.com/rust-lang/crates.io-index" 1211 | checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" 1212 | dependencies = [ 1213 | "rand_core 0.3.1", 1214 | ] 1215 | 1216 | [[package]] 1217 | name = "redox_syscall" 1218 | version = "0.2.10" 1219 | source = "registry+https://github.com/rust-lang/crates.io-index" 1220 | checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" 1221 | dependencies = [ 1222 | "bitflags", 1223 | ] 1224 | 1225 | [[package]] 1226 | name = "regex" 1227 | version = "1.5.4" 1228 | source = "registry+https://github.com/rust-lang/crates.io-index" 1229 | checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" 1230 | dependencies = [ 1231 | "aho-corasick", 1232 | "memchr", 1233 | "regex-syntax", 1234 | ] 1235 | 1236 | [[package]] 1237 | name = "regex-syntax" 1238 | version = "0.6.25" 1239 | source = "registry+https://github.com/rust-lang/crates.io-index" 1240 | checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" 1241 | 1242 | [[package]] 1243 | name = "remove_dir_all" 1244 | version = "0.5.3" 1245 | source = "registry+https://github.com/rust-lang/crates.io-index" 1246 | checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" 1247 | dependencies = [ 1248 | "winapi", 1249 | ] 1250 | 1251 | [[package]] 1252 | name = "reqwest" 1253 | version = "0.11.4" 1254 | source = "registry+https://github.com/rust-lang/crates.io-index" 1255 | checksum = "246e9f61b9bb77df069a947682be06e31ac43ea37862e244a69f177694ea6d22" 1256 | dependencies = [ 1257 | "base64", 1258 | "bytes", 1259 | "encoding_rs", 1260 | "futures-core", 1261 | "futures-util", 1262 | "http", 1263 | "http-body", 1264 | "hyper", 1265 | "hyper-tls", 1266 | "ipnet", 1267 | "js-sys", 1268 | "lazy_static", 1269 | "log", 1270 | "mime", 1271 | "mime_guess", 1272 | "native-tls", 1273 | "percent-encoding", 1274 | "pin-project-lite", 1275 | "serde", 1276 | "serde_json", 1277 | "serde_urlencoded", 1278 | "tokio", 1279 | "tokio-native-tls", 1280 | "url", 1281 | "wasm-bindgen", 1282 | "wasm-bindgen-futures", 1283 | "web-sys", 1284 | "winreg", 1285 | ] 1286 | 1287 | [[package]] 1288 | name = "rustc_version" 1289 | version = "0.2.3" 1290 | source = "registry+https://github.com/rust-lang/crates.io-index" 1291 | checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" 1292 | dependencies = [ 1293 | "semver", 1294 | ] 1295 | 1296 | [[package]] 1297 | name = "ryu" 1298 | version = "1.0.5" 1299 | source = "registry+https://github.com/rust-lang/crates.io-index" 1300 | checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" 1301 | 1302 | [[package]] 1303 | name = "same-file" 1304 | version = "1.0.6" 1305 | source = "registry+https://github.com/rust-lang/crates.io-index" 1306 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 1307 | dependencies = [ 1308 | "winapi-util", 1309 | ] 1310 | 1311 | [[package]] 1312 | name = "schannel" 1313 | version = "0.1.19" 1314 | source = "registry+https://github.com/rust-lang/crates.io-index" 1315 | checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" 1316 | dependencies = [ 1317 | "lazy_static", 1318 | "winapi", 1319 | ] 1320 | 1321 | [[package]] 1322 | name = "scopeguard" 1323 | version = "1.1.0" 1324 | source = "registry+https://github.com/rust-lang/crates.io-index" 1325 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 1326 | 1327 | [[package]] 1328 | name = "security-framework" 1329 | version = "2.4.2" 1330 | source = "registry+https://github.com/rust-lang/crates.io-index" 1331 | checksum = "525bc1abfda2e1998d152c45cf13e696f76d0a4972310b22fac1658b05df7c87" 1332 | dependencies = [ 1333 | "bitflags", 1334 | "core-foundation", 1335 | "core-foundation-sys", 1336 | "libc", 1337 | "security-framework-sys", 1338 | ] 1339 | 1340 | [[package]] 1341 | name = "security-framework-sys" 1342 | version = "2.4.2" 1343 | source = "registry+https://github.com/rust-lang/crates.io-index" 1344 | checksum = "a9dd14d83160b528b7bfd66439110573efcfbe281b17fc2ca9f39f550d619c7e" 1345 | dependencies = [ 1346 | "core-foundation-sys", 1347 | "libc", 1348 | ] 1349 | 1350 | [[package]] 1351 | name = "semver" 1352 | version = "0.9.0" 1353 | source = "registry+https://github.com/rust-lang/crates.io-index" 1354 | checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" 1355 | dependencies = [ 1356 | "semver-parser", 1357 | ] 1358 | 1359 | [[package]] 1360 | name = "semver-parser" 1361 | version = "0.7.0" 1362 | source = "registry+https://github.com/rust-lang/crates.io-index" 1363 | checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" 1364 | 1365 | [[package]] 1366 | name = "serde" 1367 | version = "1.0.130" 1368 | source = "registry+https://github.com/rust-lang/crates.io-index" 1369 | checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" 1370 | dependencies = [ 1371 | "serde_derive", 1372 | ] 1373 | 1374 | [[package]] 1375 | name = "serde_derive" 1376 | version = "1.0.130" 1377 | source = "registry+https://github.com/rust-lang/crates.io-index" 1378 | checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b" 1379 | dependencies = [ 1380 | "proc-macro2", 1381 | "quote", 1382 | "syn", 1383 | ] 1384 | 1385 | [[package]] 1386 | name = "serde_json" 1387 | version = "1.0.67" 1388 | source = "registry+https://github.com/rust-lang/crates.io-index" 1389 | checksum = "a7f9e390c27c3c0ce8bc5d725f6e4d30a29d26659494aa4b17535f7522c5c950" 1390 | dependencies = [ 1391 | "itoa", 1392 | "ryu", 1393 | "serde", 1394 | ] 1395 | 1396 | [[package]] 1397 | name = "serde_test" 1398 | version = "1.0.130" 1399 | source = "registry+https://github.com/rust-lang/crates.io-index" 1400 | checksum = "d82178225dbdeae2d5d190e8649287db6a3a32c6d24da22ae3146325aa353e4c" 1401 | dependencies = [ 1402 | "serde", 1403 | ] 1404 | 1405 | [[package]] 1406 | name = "serde_urlencoded" 1407 | version = "0.7.0" 1408 | source = "registry+https://github.com/rust-lang/crates.io-index" 1409 | checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9" 1410 | dependencies = [ 1411 | "form_urlencoded", 1412 | "itoa", 1413 | "ryu", 1414 | "serde", 1415 | ] 1416 | 1417 | [[package]] 1418 | name = "serde_yaml" 1419 | version = "0.8.20" 1420 | source = "registry+https://github.com/rust-lang/crates.io-index" 1421 | checksum = "ad104641f3c958dab30eb3010e834c2622d1f3f4c530fef1dee20ad9485f3c09" 1422 | dependencies = [ 1423 | "dtoa", 1424 | "indexmap", 1425 | "serde", 1426 | "yaml-rust", 1427 | ] 1428 | 1429 | [[package]] 1430 | name = "sha-1" 1431 | version = "0.8.2" 1432 | source = "registry+https://github.com/rust-lang/crates.io-index" 1433 | checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df" 1434 | dependencies = [ 1435 | "block-buffer", 1436 | "digest", 1437 | "fake-simd", 1438 | "opaque-debug", 1439 | ] 1440 | 1441 | [[package]] 1442 | name = "sha1" 1443 | version = "0.6.0" 1444 | source = "registry+https://github.com/rust-lang/crates.io-index" 1445 | checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d" 1446 | 1447 | [[package]] 1448 | name = "signal-hook-registry" 1449 | version = "1.4.0" 1450 | source = "registry+https://github.com/rust-lang/crates.io-index" 1451 | checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" 1452 | dependencies = [ 1453 | "libc", 1454 | ] 1455 | 1456 | [[package]] 1457 | name = "slab" 1458 | version = "0.4.4" 1459 | source = "registry+https://github.com/rust-lang/crates.io-index" 1460 | checksum = "c307a32c1c5c437f38c7fd45d753050587732ba8628319fbdf12a7e289ccc590" 1461 | 1462 | [[package]] 1463 | name = "slug" 1464 | version = "0.1.4" 1465 | source = "registry+https://github.com/rust-lang/crates.io-index" 1466 | checksum = "b3bc762e6a4b6c6fcaade73e77f9ebc6991b676f88bb2358bddb56560f073373" 1467 | dependencies = [ 1468 | "deunicode", 1469 | ] 1470 | 1471 | [[package]] 1472 | name = "smallvec" 1473 | version = "0.6.14" 1474 | source = "registry+https://github.com/rust-lang/crates.io-index" 1475 | checksum = "b97fcaeba89edba30f044a10c6a3cc39df9c3f17d7cd829dd1446cab35f890e0" 1476 | dependencies = [ 1477 | "maybe-uninit", 1478 | ] 1479 | 1480 | [[package]] 1481 | name = "smallvec" 1482 | version = "1.6.1" 1483 | source = "registry+https://github.com/rust-lang/crates.io-index" 1484 | checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" 1485 | 1486 | [[package]] 1487 | name = "socket2" 1488 | version = "0.4.1" 1489 | source = "registry+https://github.com/rust-lang/crates.io-index" 1490 | checksum = "765f090f0e423d2b55843402a07915add955e7d60657db13707a159727326cad" 1491 | dependencies = [ 1492 | "libc", 1493 | "winapi", 1494 | ] 1495 | 1496 | [[package]] 1497 | name = "stable_deref_trait" 1498 | version = "1.2.0" 1499 | source = "registry+https://github.com/rust-lang/crates.io-index" 1500 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 1501 | 1502 | [[package]] 1503 | name = "standback" 1504 | version = "0.2.17" 1505 | source = "registry+https://github.com/rust-lang/crates.io-index" 1506 | checksum = "e113fb6f3de07a243d434a56ec6f186dfd51cb08448239fe7bcae73f87ff28ff" 1507 | dependencies = [ 1508 | "version_check", 1509 | ] 1510 | 1511 | [[package]] 1512 | name = "stdweb" 1513 | version = "0.4.20" 1514 | source = "registry+https://github.com/rust-lang/crates.io-index" 1515 | checksum = "d022496b16281348b52d0e30ae99e01a73d737b2f45d38fed4edf79f9325a1d5" 1516 | dependencies = [ 1517 | "discard", 1518 | "rustc_version", 1519 | "stdweb-derive", 1520 | "stdweb-internal-macros", 1521 | "stdweb-internal-runtime", 1522 | "wasm-bindgen", 1523 | ] 1524 | 1525 | [[package]] 1526 | name = "stdweb-derive" 1527 | version = "0.5.3" 1528 | source = "registry+https://github.com/rust-lang/crates.io-index" 1529 | checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef" 1530 | dependencies = [ 1531 | "proc-macro2", 1532 | "quote", 1533 | "serde", 1534 | "serde_derive", 1535 | "syn", 1536 | ] 1537 | 1538 | [[package]] 1539 | name = "stdweb-internal-macros" 1540 | version = "0.2.9" 1541 | source = "registry+https://github.com/rust-lang/crates.io-index" 1542 | checksum = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11" 1543 | dependencies = [ 1544 | "base-x", 1545 | "proc-macro2", 1546 | "quote", 1547 | "serde", 1548 | "serde_derive", 1549 | "serde_json", 1550 | "sha1", 1551 | "syn", 1552 | ] 1553 | 1554 | [[package]] 1555 | name = "stdweb-internal-runtime" 1556 | version = "0.1.5" 1557 | source = "registry+https://github.com/rust-lang/crates.io-index" 1558 | checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" 1559 | 1560 | [[package]] 1561 | name = "strsim" 1562 | version = "0.8.0" 1563 | source = "registry+https://github.com/rust-lang/crates.io-index" 1564 | checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" 1565 | 1566 | [[package]] 1567 | name = "structopt" 1568 | version = "0.3.23" 1569 | source = "registry+https://github.com/rust-lang/crates.io-index" 1570 | checksum = "bf9d950ef167e25e0bdb073cf1d68e9ad2795ac826f2f3f59647817cf23c0bfa" 1571 | dependencies = [ 1572 | "clap", 1573 | "lazy_static", 1574 | "structopt-derive", 1575 | ] 1576 | 1577 | [[package]] 1578 | name = "structopt-derive" 1579 | version = "0.4.16" 1580 | source = "registry+https://github.com/rust-lang/crates.io-index" 1581 | checksum = "134d838a2c9943ac3125cf6df165eda53493451b719f3255b2a26b85f772d0ba" 1582 | dependencies = [ 1583 | "heck", 1584 | "proc-macro-error", 1585 | "proc-macro2", 1586 | "quote", 1587 | "syn", 1588 | ] 1589 | 1590 | [[package]] 1591 | name = "syn" 1592 | version = "1.0.76" 1593 | source = "registry+https://github.com/rust-lang/crates.io-index" 1594 | checksum = "c6f107db402c2c2055242dbf4d2af0e69197202e9faacbef9571bbe47f5a1b84" 1595 | dependencies = [ 1596 | "proc-macro2", 1597 | "quote", 1598 | "unicode-xid", 1599 | ] 1600 | 1601 | [[package]] 1602 | name = "sys-info" 1603 | version = "0.8.0" 1604 | source = "registry+https://github.com/rust-lang/crates.io-index" 1605 | checksum = "3f3e7ba888a12ddcf0084e36ae4609b055845f38022d1946b67356fbc27d5795" 1606 | dependencies = [ 1607 | "cc", 1608 | "libc", 1609 | ] 1610 | 1611 | [[package]] 1612 | name = "tempfile" 1613 | version = "3.2.0" 1614 | source = "registry+https://github.com/rust-lang/crates.io-index" 1615 | checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" 1616 | dependencies = [ 1617 | "cfg-if", 1618 | "libc", 1619 | "rand 0.8.4", 1620 | "redox_syscall", 1621 | "remove_dir_all", 1622 | "winapi", 1623 | ] 1624 | 1625 | [[package]] 1626 | name = "tera" 1627 | version = "1.12.1" 1628 | source = "registry+https://github.com/rust-lang/crates.io-index" 1629 | checksum = "bf95b0d8a46da5fe3ea119394a6c7f1e745f9de359081641c99946e2bf55d4f2" 1630 | dependencies = [ 1631 | "chrono", 1632 | "chrono-tz", 1633 | "globwalk", 1634 | "humansize", 1635 | "lazy_static", 1636 | "percent-encoding", 1637 | "pest", 1638 | "pest_derive", 1639 | "rand 0.8.4", 1640 | "regex", 1641 | "serde", 1642 | "serde_json", 1643 | "slug", 1644 | "unic-segment", 1645 | ] 1646 | 1647 | [[package]] 1648 | name = "termcolor" 1649 | version = "1.1.2" 1650 | source = "registry+https://github.com/rust-lang/crates.io-index" 1651 | checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" 1652 | dependencies = [ 1653 | "winapi-util", 1654 | ] 1655 | 1656 | [[package]] 1657 | name = "textwrap" 1658 | version = "0.11.0" 1659 | source = "registry+https://github.com/rust-lang/crates.io-index" 1660 | checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" 1661 | dependencies = [ 1662 | "unicode-width", 1663 | ] 1664 | 1665 | [[package]] 1666 | name = "thread_local" 1667 | version = "1.1.3" 1668 | source = "registry+https://github.com/rust-lang/crates.io-index" 1669 | checksum = "8018d24e04c95ac8790716a5987d0fec4f8b27249ffa0f7d33f1369bdfb88cbd" 1670 | dependencies = [ 1671 | "once_cell", 1672 | ] 1673 | 1674 | [[package]] 1675 | name = "time" 1676 | version = "0.1.44" 1677 | source = "registry+https://github.com/rust-lang/crates.io-index" 1678 | checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" 1679 | dependencies = [ 1680 | "libc", 1681 | "wasi", 1682 | "winapi", 1683 | ] 1684 | 1685 | [[package]] 1686 | name = "time" 1687 | version = "0.2.27" 1688 | source = "registry+https://github.com/rust-lang/crates.io-index" 1689 | checksum = "4752a97f8eebd6854ff91f1c1824cd6160626ac4bd44287f7f4ea2035a02a242" 1690 | dependencies = [ 1691 | "const_fn", 1692 | "libc", 1693 | "standback", 1694 | "stdweb", 1695 | "time-macros", 1696 | "version_check", 1697 | "winapi", 1698 | ] 1699 | 1700 | [[package]] 1701 | name = "time-macros" 1702 | version = "0.1.1" 1703 | source = "registry+https://github.com/rust-lang/crates.io-index" 1704 | checksum = "957e9c6e26f12cb6d0dd7fc776bb67a706312e7299aed74c8dd5b17ebb27e2f1" 1705 | dependencies = [ 1706 | "proc-macro-hack", 1707 | "time-macros-impl", 1708 | ] 1709 | 1710 | [[package]] 1711 | name = "time-macros-impl" 1712 | version = "0.1.2" 1713 | source = "registry+https://github.com/rust-lang/crates.io-index" 1714 | checksum = "fd3c141a1b43194f3f56a1411225df8646c55781d5f26db825b3d98507eb482f" 1715 | dependencies = [ 1716 | "proc-macro-hack", 1717 | "proc-macro2", 1718 | "quote", 1719 | "standback", 1720 | "syn", 1721 | ] 1722 | 1723 | [[package]] 1724 | name = "tinyvec" 1725 | version = "1.3.1" 1726 | source = "registry+https://github.com/rust-lang/crates.io-index" 1727 | checksum = "848a1e1181b9f6753b5e96a092749e29b11d19ede67dfbbd6c7dc7e0f49b5338" 1728 | dependencies = [ 1729 | "tinyvec_macros", 1730 | ] 1731 | 1732 | [[package]] 1733 | name = "tinyvec_macros" 1734 | version = "0.1.0" 1735 | source = "registry+https://github.com/rust-lang/crates.io-index" 1736 | checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" 1737 | 1738 | [[package]] 1739 | name = "tokio" 1740 | version = "1.11.0" 1741 | source = "registry+https://github.com/rust-lang/crates.io-index" 1742 | checksum = "b4efe6fc2395938c8155973d7be49fe8d03a843726e285e100a8a383cc0154ce" 1743 | dependencies = [ 1744 | "autocfg", 1745 | "bytes", 1746 | "libc", 1747 | "memchr", 1748 | "mio", 1749 | "num_cpus", 1750 | "once_cell", 1751 | "parking_lot 0.11.2", 1752 | "pin-project-lite", 1753 | "signal-hook-registry", 1754 | "tokio-macros", 1755 | "winapi", 1756 | ] 1757 | 1758 | [[package]] 1759 | name = "tokio-macros" 1760 | version = "1.3.0" 1761 | source = "registry+https://github.com/rust-lang/crates.io-index" 1762 | checksum = "54473be61f4ebe4efd09cec9bd5d16fa51d70ea0192213d754d2d500457db110" 1763 | dependencies = [ 1764 | "proc-macro2", 1765 | "quote", 1766 | "syn", 1767 | ] 1768 | 1769 | [[package]] 1770 | name = "tokio-native-tls" 1771 | version = "0.3.0" 1772 | source = "registry+https://github.com/rust-lang/crates.io-index" 1773 | checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" 1774 | dependencies = [ 1775 | "native-tls", 1776 | "tokio", 1777 | ] 1778 | 1779 | [[package]] 1780 | name = "tokio-util" 1781 | version = "0.6.8" 1782 | source = "registry+https://github.com/rust-lang/crates.io-index" 1783 | checksum = "08d3725d3efa29485e87311c5b699de63cde14b00ed4d256b8318aa30ca452cd" 1784 | dependencies = [ 1785 | "bytes", 1786 | "futures-core", 1787 | "futures-sink", 1788 | "log", 1789 | "pin-project-lite", 1790 | "tokio", 1791 | ] 1792 | 1793 | [[package]] 1794 | name = "tower-service" 1795 | version = "0.3.1" 1796 | source = "registry+https://github.com/rust-lang/crates.io-index" 1797 | checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" 1798 | 1799 | [[package]] 1800 | name = "tracing" 1801 | version = "0.1.26" 1802 | source = "registry+https://github.com/rust-lang/crates.io-index" 1803 | checksum = "09adeb8c97449311ccd28a427f96fb563e7fd31aabf994189879d9da2394b89d" 1804 | dependencies = [ 1805 | "cfg-if", 1806 | "pin-project-lite", 1807 | "tracing-core", 1808 | ] 1809 | 1810 | [[package]] 1811 | name = "tracing-core" 1812 | version = "0.1.19" 1813 | source = "registry+https://github.com/rust-lang/crates.io-index" 1814 | checksum = "2ca517f43f0fb96e0c3072ed5c275fe5eece87e8cb52f4a77b69226d3b1c9df8" 1815 | dependencies = [ 1816 | "lazy_static", 1817 | ] 1818 | 1819 | [[package]] 1820 | name = "try-lock" 1821 | version = "0.2.3" 1822 | source = "registry+https://github.com/rust-lang/crates.io-index" 1823 | checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" 1824 | 1825 | [[package]] 1826 | name = "typenum" 1827 | version = "1.14.0" 1828 | source = "registry+https://github.com/rust-lang/crates.io-index" 1829 | checksum = "b63708a265f51345575b27fe43f9500ad611579e764c79edbc2037b1121959ec" 1830 | 1831 | [[package]] 1832 | name = "ucd-trie" 1833 | version = "0.1.3" 1834 | source = "registry+https://github.com/rust-lang/crates.io-index" 1835 | checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" 1836 | 1837 | [[package]] 1838 | name = "unic-char-property" 1839 | version = "0.9.0" 1840 | source = "registry+https://github.com/rust-lang/crates.io-index" 1841 | checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" 1842 | dependencies = [ 1843 | "unic-char-range", 1844 | ] 1845 | 1846 | [[package]] 1847 | name = "unic-char-range" 1848 | version = "0.9.0" 1849 | source = "registry+https://github.com/rust-lang/crates.io-index" 1850 | checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" 1851 | 1852 | [[package]] 1853 | name = "unic-common" 1854 | version = "0.9.0" 1855 | source = "registry+https://github.com/rust-lang/crates.io-index" 1856 | checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" 1857 | 1858 | [[package]] 1859 | name = "unic-segment" 1860 | version = "0.9.0" 1861 | source = "registry+https://github.com/rust-lang/crates.io-index" 1862 | checksum = "e4ed5d26be57f84f176157270c112ef57b86debac9cd21daaabbe56db0f88f23" 1863 | dependencies = [ 1864 | "unic-ucd-segment", 1865 | ] 1866 | 1867 | [[package]] 1868 | name = "unic-ucd-segment" 1869 | version = "0.9.0" 1870 | source = "registry+https://github.com/rust-lang/crates.io-index" 1871 | checksum = "2079c122a62205b421f499da10f3ee0f7697f012f55b675e002483c73ea34700" 1872 | dependencies = [ 1873 | "unic-char-property", 1874 | "unic-char-range", 1875 | "unic-ucd-version", 1876 | ] 1877 | 1878 | [[package]] 1879 | name = "unic-ucd-version" 1880 | version = "0.9.0" 1881 | source = "registry+https://github.com/rust-lang/crates.io-index" 1882 | checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" 1883 | dependencies = [ 1884 | "unic-common", 1885 | ] 1886 | 1887 | [[package]] 1888 | name = "unicase" 1889 | version = "2.6.0" 1890 | source = "registry+https://github.com/rust-lang/crates.io-index" 1891 | checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" 1892 | dependencies = [ 1893 | "version_check", 1894 | ] 1895 | 1896 | [[package]] 1897 | name = "unicode-bidi" 1898 | version = "0.3.6" 1899 | source = "registry+https://github.com/rust-lang/crates.io-index" 1900 | checksum = "246f4c42e67e7a4e3c6106ff716a5d067d4132a642840b242e357e468a2a0085" 1901 | 1902 | [[package]] 1903 | name = "unicode-normalization" 1904 | version = "0.1.19" 1905 | source = "registry+https://github.com/rust-lang/crates.io-index" 1906 | checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" 1907 | dependencies = [ 1908 | "tinyvec", 1909 | ] 1910 | 1911 | [[package]] 1912 | name = "unicode-segmentation" 1913 | version = "1.8.0" 1914 | source = "registry+https://github.com/rust-lang/crates.io-index" 1915 | checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" 1916 | 1917 | [[package]] 1918 | name = "unicode-width" 1919 | version = "0.1.8" 1920 | source = "registry+https://github.com/rust-lang/crates.io-index" 1921 | checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" 1922 | 1923 | [[package]] 1924 | name = "unicode-xid" 1925 | version = "0.2.2" 1926 | source = "registry+https://github.com/rust-lang/crates.io-index" 1927 | checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" 1928 | 1929 | [[package]] 1930 | name = "url" 1931 | version = "2.2.2" 1932 | source = "registry+https://github.com/rust-lang/crates.io-index" 1933 | checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" 1934 | dependencies = [ 1935 | "form_urlencoded", 1936 | "idna", 1937 | "matches", 1938 | "percent-encoding", 1939 | ] 1940 | 1941 | [[package]] 1942 | name = "vcpkg" 1943 | version = "0.2.15" 1944 | source = "registry+https://github.com/rust-lang/crates.io-index" 1945 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 1946 | 1947 | [[package]] 1948 | name = "vec_map" 1949 | version = "0.8.2" 1950 | source = "registry+https://github.com/rust-lang/crates.io-index" 1951 | checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" 1952 | 1953 | [[package]] 1954 | name = "version_check" 1955 | version = "0.9.3" 1956 | source = "registry+https://github.com/rust-lang/crates.io-index" 1957 | checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" 1958 | 1959 | [[package]] 1960 | name = "walkdir" 1961 | version = "2.3.2" 1962 | source = "registry+https://github.com/rust-lang/crates.io-index" 1963 | checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" 1964 | dependencies = [ 1965 | "same-file", 1966 | "winapi", 1967 | "winapi-util", 1968 | ] 1969 | 1970 | [[package]] 1971 | name = "want" 1972 | version = "0.3.0" 1973 | source = "registry+https://github.com/rust-lang/crates.io-index" 1974 | checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" 1975 | dependencies = [ 1976 | "log", 1977 | "try-lock", 1978 | ] 1979 | 1980 | [[package]] 1981 | name = "wasi" 1982 | version = "0.10.0+wasi-snapshot-preview1" 1983 | source = "registry+https://github.com/rust-lang/crates.io-index" 1984 | checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" 1985 | 1986 | [[package]] 1987 | name = "wasm-bindgen" 1988 | version = "0.2.76" 1989 | source = "registry+https://github.com/rust-lang/crates.io-index" 1990 | checksum = "8ce9b1b516211d33767048e5d47fa2a381ed8b76fc48d2ce4aa39877f9f183e0" 1991 | dependencies = [ 1992 | "cfg-if", 1993 | "serde", 1994 | "serde_json", 1995 | "wasm-bindgen-macro", 1996 | ] 1997 | 1998 | [[package]] 1999 | name = "wasm-bindgen-backend" 2000 | version = "0.2.76" 2001 | source = "registry+https://github.com/rust-lang/crates.io-index" 2002 | checksum = "cfe8dc78e2326ba5f845f4b5bf548401604fa20b1dd1d365fb73b6c1d6364041" 2003 | dependencies = [ 2004 | "bumpalo", 2005 | "lazy_static", 2006 | "log", 2007 | "proc-macro2", 2008 | "quote", 2009 | "syn", 2010 | "wasm-bindgen-shared", 2011 | ] 2012 | 2013 | [[package]] 2014 | name = "wasm-bindgen-futures" 2015 | version = "0.4.26" 2016 | source = "registry+https://github.com/rust-lang/crates.io-index" 2017 | checksum = "95fded345a6559c2cfee778d562300c581f7d4ff3edb9b0d230d69800d213972" 2018 | dependencies = [ 2019 | "cfg-if", 2020 | "js-sys", 2021 | "wasm-bindgen", 2022 | "web-sys", 2023 | ] 2024 | 2025 | [[package]] 2026 | name = "wasm-bindgen-macro" 2027 | version = "0.2.76" 2028 | source = "registry+https://github.com/rust-lang/crates.io-index" 2029 | checksum = "44468aa53335841d9d6b6c023eaab07c0cd4bddbcfdee3e2bb1e8d2cb8069fef" 2030 | dependencies = [ 2031 | "quote", 2032 | "wasm-bindgen-macro-support", 2033 | ] 2034 | 2035 | [[package]] 2036 | name = "wasm-bindgen-macro-support" 2037 | version = "0.2.76" 2038 | source = "registry+https://github.com/rust-lang/crates.io-index" 2039 | checksum = "0195807922713af1e67dc66132c7328206ed9766af3858164fb583eedc25fbad" 2040 | dependencies = [ 2041 | "proc-macro2", 2042 | "quote", 2043 | "syn", 2044 | "wasm-bindgen-backend", 2045 | "wasm-bindgen-shared", 2046 | ] 2047 | 2048 | [[package]] 2049 | name = "wasm-bindgen-shared" 2050 | version = "0.2.76" 2051 | source = "registry+https://github.com/rust-lang/crates.io-index" 2052 | checksum = "acdb075a845574a1fa5f09fd77e43f7747599301ea3417a9fbffdeedfc1f4a29" 2053 | 2054 | [[package]] 2055 | name = "web-sys" 2056 | version = "0.3.53" 2057 | source = "registry+https://github.com/rust-lang/crates.io-index" 2058 | checksum = "224b2f6b67919060055ef1a67807367c2066ed520c3862cc013d26cf893a783c" 2059 | dependencies = [ 2060 | "js-sys", 2061 | "wasm-bindgen", 2062 | ] 2063 | 2064 | [[package]] 2065 | name = "winapi" 2066 | version = "0.3.9" 2067 | source = "registry+https://github.com/rust-lang/crates.io-index" 2068 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 2069 | dependencies = [ 2070 | "winapi-i686-pc-windows-gnu", 2071 | "winapi-x86_64-pc-windows-gnu", 2072 | ] 2073 | 2074 | [[package]] 2075 | name = "winapi-i686-pc-windows-gnu" 2076 | version = "0.4.0" 2077 | source = "registry+https://github.com/rust-lang/crates.io-index" 2078 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 2079 | 2080 | [[package]] 2081 | name = "winapi-util" 2082 | version = "0.1.5" 2083 | source = "registry+https://github.com/rust-lang/crates.io-index" 2084 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 2085 | dependencies = [ 2086 | "winapi", 2087 | ] 2088 | 2089 | [[package]] 2090 | name = "winapi-x86_64-pc-windows-gnu" 2091 | version = "0.4.0" 2092 | source = "registry+https://github.com/rust-lang/crates.io-index" 2093 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 2094 | 2095 | [[package]] 2096 | name = "winreg" 2097 | version = "0.7.0" 2098 | source = "registry+https://github.com/rust-lang/crates.io-index" 2099 | checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" 2100 | dependencies = [ 2101 | "winapi", 2102 | ] 2103 | 2104 | [[package]] 2105 | name = "yaml-rust" 2106 | version = "0.4.5" 2107 | source = "registry+https://github.com/rust-lang/crates.io-index" 2108 | checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" 2109 | dependencies = [ 2110 | "linked-hash-map", 2111 | ] 2112 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lorikeet" 3 | version = "0.15.0" 4 | authors = ["cetra3 "] 5 | license = "MIT/Apache-2.0" 6 | description = "a parallel test runner for devops" 7 | repository = "https://github.com/cetra3/lorikeet" 8 | readme = "README.md" 9 | edition = "2018" 10 | 11 | [dependencies] 12 | petgraph = "0.5.1" 13 | serde = { version = "1.0", features = ["derive"] } 14 | serde_yaml = "0.8.16" 15 | linked-hash-map = { version = "0.5.4", features = ["serde_impl"] } 16 | tera = "1.6.1" 17 | structopt = "0.3.21" 18 | log = "0.4.14" 19 | colored = "2.0.0" 20 | atty = "0.2.14" 21 | regex = "1.4.3" 22 | env_logger = "0.8.2" 23 | reqwest = {version = "0.11.0", features = ["json", "stream", "multipart"] } 24 | chrono = { version = "0.4.19", features = ["serde"] } 25 | lazy_static = "1.4.0" 26 | chashmap = "2.2.2" 27 | hostname = "0.3.1" 28 | sys-info = "0.8.0" 29 | openssl-probe = "0.1.2" 30 | jmespath = "0.3.0" 31 | anyhow = "1.0.38" 32 | serde_json = "1.0" 33 | quick-xml = "0.21.0" 34 | cookie = "0.14.3" 35 | tokio = {version = "1.0", features = ["full"]} 36 | tokio-util = {version = "0.6", features= ["codec"]} 37 | libc = "0.2.86" 38 | futures = "0.3.15" 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | 6 | # Lorikeet 7 | 8 | A Parallel test runner for DevOps. 9 | 10 | 11 | ## Download 12 | 13 | Download the latest binary for linux or osx from here: [https://github.com/cetra3/lorikeet/releases](https://github.com/cetra3/lorikeet/releases) 14 | 15 | ## Overview 16 | 17 | Lorikeet is a command line tool and a rust library to run tests for smoke testing and integration testing. Lorikeet currently supports bash commands and simple http requests along with system information (ram, cpu). 18 | 19 | Test plans are defined within a yaml file and can be templated using tera. Each step within a test plan can have multiple dependencies (run some steps before others) and can have expectations about the output of each command. 20 | 21 | Steps are run in parallel by default, using the number of threads that are available to your system. If a step has dependencies either by `require` or `required_by` attributes, then it will wait until those steps are finished. 22 | 23 | As an example, here's a test plan to check to see whether reddit is up, and then tries to login if it is: 24 | 25 | ```yaml 26 | check_reddit: 27 | http: https://www.reddit.com 28 | regex: the front page of the internet 29 | 30 | login_to_reddit: 31 | http: 32 | url: https://www.reddit.com/api/login/{{user}} 33 | save_cookies: true 34 | form: 35 | user: {{user}} 36 | passwd: {{pass}} 37 | api_type: json 38 | jmespath: length(json.errors) 39 | matches: 0 40 | require: 41 | - check_reddit 42 | ``` 43 | 44 | ( As a side note, we have added `jmespath: length(json.errors)` & `matches: 0` because an invalid login to reddit still returns a status of `200 OK` ) 45 | 46 | And the output of lorikeet: 47 | 48 | ```yaml 49 | $ lorikeet -c config.yml test.yml 50 | - name: check_reddit 51 | pass: true 52 | output: the front page of the internet 53 | duration: 1416.591ms 54 | 55 | - name: login_to_reddit 56 | pass: true 57 | output: 0 58 | duration: 1089.0276ms 59 | ``` 60 | 61 | The name comes from the [Rainbow Lorikeet](https://en.wikipedia.org/wiki/Rainbow_lorikeet), an Australian Bird which is very colourful. Like a canary in a coal mine, lorikeet is meant to provide a way of notifying when things go wrong. Rather than running one test framework (one colour), it is meant to be more full spectrum, hence the choice of a bird with rainbow plumage. 62 | 63 | They are also very noisy birds. 64 | 65 | ## Changes in `0.15.0` 66 | 67 | * Add in a new option to run a step on failure: 68 | 69 | ```yaml 70 | on_fail_example: 71 | value: true 72 | matches: false 73 | on_fail: 74 | bash: notify-send "Lorikeet Failed!" 75 | ``` 76 | 77 | * We now have [releases](https://github.com/cetra3/lorikeet/releases) being generated via github actions 78 | 79 | ## Changes in `0.14.0` 80 | 81 | * Breaking Change: Add in a default timeout (`timeout_ms`) for http requests to `30000` milliseconds (30 seconds), This default can be changed as per http options below. 82 | 83 | 84 | ## Changes in `0.13.1` 85 | 86 | * Adds slack webhook support. if there are any steps that have errors, this will be sent to a webhook: 87 | 88 | ``` 89 | lorikeet -s https://hooks.slack.com/services/ test 90 | ``` 91 | 92 | ## Changes in `0.13.0` 93 | 94 | * Breaking Change: The `run_steps` method returns a stream of steps as they complete, rather than waiting for them all to finish 95 | * More Clippy Lints 96 | * Fixed a compilation error on osx 97 | 98 | ## Changes in `0.12.1` 99 | 100 | * Fixed all clippy issues 101 | * JMESPath strings are no longer quoted 102 | * Added ability to include step output in subsequent requests. You can do this by including `${step_output.}` where `` is the name of the step to include as output. Currently you will still need to "require" this step 103 | 104 | ```yaml 105 | example_output: 106 | require: another_step 107 | http: 108 | url: http://example.com 109 | body: | 110 | ${step_output.another_step} 111 | ``` 112 | 113 | ## Changes in `0.12.0` 114 | 115 | * Update to Tokio 1.0 116 | * Updates to all library dependencies 117 | 118 | ## Changes in `0.11.0` 119 | 120 | * Initial Async Version 121 | * Updates to library dependencies 122 | 123 | ## Changes in `0.10.0` 124 | 125 | * Upgrade to 2018 crate format 126 | * Fixed terminal painting on Ubuntu 19.10 127 | * A few minor updates to the library version 128 | 129 | ## Changes in `0.9.0` 130 | 131 | * Upgrade to Reqwest `0.9.x` branch, thanks [norcali](https://github.com/norcalli)! 132 | 133 | * Added multipart support, body, and headers support to the HTTP request type: 134 | 135 | To add custom headers, supply a map of `header_name: header_value`: 136 | 137 | ```yaml 138 | Example Header: 139 | http: 140 | url: https://example.com 141 | headers: 142 | my-custom-header: my-custom-value 143 | ``` 144 | 145 | Multipart works in the same way as the existing `form` option, but allows you to also specify files to upload: 146 | 147 | ```yaml 148 | Example Multipart: 149 | http: 150 | url: https://example.com 151 | multipart: 152 | multipart_field: multipart_value 153 | file_upload: 154 | file: /path/to/file 155 | ``` 156 | 157 | You can also just set a generic body via a string: 158 | 159 | ```yaml 160 | Example Body: 161 | http: 162 | url: https://example.com 163 | body: | 164 | This is a generic POST body 165 | ``` 166 | 167 | ## Changes in `0.8.0` 168 | 169 | * The cli app will not panic if there is an issue reading, parsing or running steps, instead it will output a `lorikeet` step to display what the error is, and still submit it via webhooks, etc.. 170 | 171 | * Added in initial delay for a step. If you want to wait an arbitrary period of time before running a step, then you can set an initial delay with the `delay_ms` parameter. This delay is only executed when the step would normally start, so if you have dependent steps, they will run first, then the delay, then the step. 172 | * Added in Retry Policy: If a test fails, you can retry n times by setting the `retry_count` property. You can also delay retries by setting the `retry_delay_ms` parameter. 173 | * Both `delay_ms` and `retry_delay_ms` are in milliseconds and must be a positive integer value. 174 | 175 | * Added initial `junit` output so you can use lorikeet with jenkins or another CI server that supports junit xml reports. Use `-j report.xml` to output junit reports. 176 | 177 | ## Changes in `0.7.0` 178 | 179 | * The main change here was to change the YAML parsing to remove panics, returning a `Result>` which is a breaking change 180 | * A new function `get_steps_raw` which takes a `&str` yaml & anything that implements `Serialize` as a config context. This mainly allows the library to be used without touching the file system for configs or steps. `get_steps` still can be provided with paths 181 | 182 | ## Installation 183 | 184 | Lorikeet is on crates.io, so you can either run: 185 | 186 | ```sh 187 | cargo install lorikeet 188 | ``` 189 | 190 | Or clone and build this repo: 191 | 192 | ```sh 193 | cargo build --release 194 | ``` 195 | 196 | Alternatively, you can download prebuilt from the [releases](https://github.com/cetra3/lorikeet/releases) page 197 | 198 | ## Usage 199 | 200 | Command line usage is given by `lorikeet -h`: 201 | 202 | ``` 203 | USAGE: 204 | lorikeet [FLAGS] [OPTIONS] [test_plan] 205 | 206 | FLAGS: 207 | -h, --help Prints help information 208 | -q, --quiet Don't output results to console 209 | -V, --version Prints version information 210 | 211 | OPTIONS: 212 | -c, --config Configuration File 213 | -j, --junit Output a JUnit XML Report to this file 214 | -w, --webhook ... Webhook submission URL (multiple values allowed) 215 | 216 | ARGS: 217 | Test Plan [default: test.yml] 218 | ``` 219 | 220 | ### Test Plan 221 | 222 | The test plan is the main driver for lorikeet and is already quite flexible. See below for examples and test syntax. By default lorikeet will expect a file `test.yml` in the current directory. 223 | 224 | ### Config Option 225 | 226 | Lorikeet uses [tera](https://github.com/Keats/tera) as a template engine so you can include variables within your yaml test plan. Using `-c` you can provide the context of the test plan as a seperate yaml file. This file can be in any shape, as long as it's valid yaml. 227 | 228 | As an example, say you want to check that a number of servers are up and connected. You can have a config like so: 229 | 230 | ```yaml 231 | instances: 232 | - server1 233 | - server2 234 | - server3 235 | ``` 236 | 237 | And then write your test plan: 238 | 239 | ```yaml 240 | {% for instance in instances %} 241 | 242 | ping_server_{{instance}}: 243 | bash: ping -c 1 {{instance}} 2>&1 >/dev/null 244 | 245 | {% endfor %} 246 | ``` 247 | 248 | And run it: 249 | 250 | ```yaml 251 | $ lorikeet -c config.yml test.yml 252 | - name: ping_server_server1 253 | pass: true 254 | duration: 7.859398ms 255 | 256 | - name: ping_server_server2 257 | pass: true 258 | duration: 7.95139ms 259 | 260 | - name: ping_server_server3 261 | pass: true 262 | duration: 7.740785ms 263 | ``` 264 | 265 | ### Webhook 266 | 267 | You can submit your results to a server using a webhook when the test run is finished. This will POST a json object with the `submitter::WebHook` shape: 268 | 269 | ```json 270 | { 271 | "hostname": "example.hostname", 272 | "has_errors": true, 273 | "tests": [{ 274 | "name": "Example Webhook", 275 | "pass": false, 276 | "output": "Example Output", 277 | "error": "Example Error", 278 | "duration": 7.70 279 | }] 280 | } 281 | ``` 282 | 283 | ## Test Plan syntax 284 | 285 | The test plan is a yaml file that is divided up into steps: 286 | 287 | ``` 288 | : 289 | : 290 | (: ) 291 | (: ) 292 | (: ) 293 | (: ) 294 | ``` 295 | 296 | Each step has a unique name and a step type. Optionally, there can be an expect type, and a list of dependencies or dependents. 297 | 298 | You can also include a description of what the test does alongside a name, so you can provide a more detailed explanation of what the test is doing 299 | 300 | ### Step Types 301 | 302 | There are currently 5 step types that can be configured: bash, http, system, step and value 303 | 304 | #### Bash Step type 305 | 306 | The bash step type simply runs the `bash` command to execute shell scripts: 307 | 308 | ```yaml 309 | say_hello: 310 | bash: echo "hello" 311 | ``` 312 | 313 | Optionally you can specify not to return the output if you're only interested in the return code of the application: 314 | 315 | ```yaml 316 | dont_say_hello: 317 | bash: 318 | cmd: echo "hello" 319 | get_output: false 320 | ``` 321 | 322 | #### HTTP Step Type 323 | 324 | The HTTP step type can execute HTTP commands to web servers using reqwest. Currently this is a very simple step type but does support status codes and storing cookies per domain. 325 | 326 | You can specify just the URL: 327 | 328 | ```yaml 329 | check_reddit: 330 | http: https://www.reddit.com 331 | matches: the front page of the internet 332 | ``` 333 | 334 | Or provide the following options: 335 | 336 | * `url`: The URL of the request to submit 337 | * `method`: The HTTP method to use, such as POST, GET, DELETE. Defaults to `GET` 338 | * `headers`: Key/Value pairs for any custom headers on your request 339 | * `get_output`: Return the output of the request. Defaults to `true` 340 | * `save_cookies`: Save any set cookies on this domain. Defaults to `false` 341 | * `status`: Check the return status is equal to this value. Defaults to `200` 342 | * `user`: Username for Basic Auth 343 | * `pass`: Password for Basic Auth 344 | * `timeout_ms`: Timeout in milliseconds for the request, defaults to `30000` (30 seconds). If set to `null` or `~` it will never timeout. 345 | * `form`: Key/Value pairs for a form POST submission. If method is set to `GET`, then this will set the method to `POST` 346 | * `multipart`: Multipart request. Key/Value pairs Like the `form` option but allows file upload as well. 347 | * `body`: Like the `form`/`multipart` options but a raw string instead of form data for JSON uploads 348 | * `verify_ssl`: Verify SSL on the remote host. Defaults to `true`. **Warning**: Disabling SSL verification will cause Lorikeet to trust _any_ host it communicates with, which can expose you to numerous vulnerabilities. You should only use this as a last resort. 349 | 350 | As a more elaborate example: 351 | 352 | ```yaml 353 | login_to_reddit: 354 | http: 355 | url: https://www.reddit.com/api/login/{{user}} 356 | save_cookies: true 357 | form: 358 | user: {{user}} 359 | passwd: {{pass}} 360 | api_type: json 361 | ``` 362 | 363 | For Multipart, you can specify files like so: 364 | 365 | ```yaml 366 | Example Multipart: 367 | http: 368 | url: https://www.example.com 369 | multipart: 370 | multipart_field: multipart_value 371 | file_upload: 372 | file: /path/to/file 373 | ``` 374 | 375 | For a JSON upload you can use the `body` field: 376 | 377 | ```yaml 378 | Example Raw JSON: 379 | http: 380 | url: https://www.example.com 381 | body: | 382 | { "json_key": "json_value" } 383 | ``` 384 | 385 | ### System Step Type 386 | 387 | The system step type will return information about the system such as available memory or system load using the sys-info crate. 388 | 389 | 390 | As an example, to check memory: 391 | ```yaml 392 | check_memory: 393 | description: Checks to see if the available memory is greater than 1gb 394 | system: mem_available 395 | greater_than: 1048000 396 | ``` 397 | 398 | The system type has a fixed list of values that returns various system info: 399 | 400 | * `load_avg_1m`: The load average over 1 minute 401 | * `load_avg_5m`: The load average over 5 minutes 402 | * `load_avg_15m`: The load average over 15 minutes 403 | * `mem_available`: The amount of available memory 404 | * `mem_free`: The amount of free memory 405 | * `mem_total`: The amount of total memory 406 | * `disk_free`: The amount of free disk space 407 | * `disk_total`: The total amount of disk space 408 | 409 | Using the `greater_than` or `less_than` expect types means you can set thresholds for environment resources: 410 | 411 | ```yaml 412 | system_load: 413 | description: Checks the System Load over the last 15 minutes is below 80% 414 | system: load_avg15m 415 | less_than: 1.6 416 | ``` 417 | 418 | #### 'Step' Step Type 419 | 420 | If you want to make more assertions on the one step, you can use the 'step' step type. This type simply returns the output of the other step: 421 | 422 | ```yaml 423 | say_hello: 424 | value: hello 425 | 426 | test_step: 427 | step: say_hello 428 | matches: hello 429 | ``` 430 | 431 | This will also implicitly require that the step it gets it output from is run first as a dependency so you don't have to worry about the order. 432 | 433 | 434 | #### Value Step Type 435 | 436 | The value step type will simply return a value, rather than executing anything. 437 | 438 | ```yaml 439 | say_hello: 440 | value: hello 441 | ``` 442 | 443 | ### Filter types 444 | 445 | You can filter your output either via regex, jmespath, or remove the output completely. Filters can be provided once off, or as a list, so you can chain filters together: 446 | 447 | ```yaml 448 | example_step: 449 | value: some example 450 | filters: 451 | - regex: some (.*) 452 | ``` 453 | 454 | You can also shorthand provide a filter on the step like so: 455 | 456 | ```yaml 457 | example_step: 458 | value: some example 459 | regex: some 460 | ``` 461 | 462 | **Note: If the filter can't match against a value, it counts as a test error** 463 | 464 | #### Regex Filter 465 | 466 | Simply filters out the output of the step based upon the matched value. 467 | 468 | ```yaml 469 | say_hello: 470 | value: hello world! 471 | regex: (.*) world! 472 | ``` 473 | 474 | You can either add it as a `regex` attribute against the step, or in the filter list: 475 | 476 | ```yaml 477 | say_hello: 478 | value: hello world! 479 | filters: 480 | - regex: (.*) world! 481 | ``` 482 | 483 | By default it will match and return the entire regex statement (`hello world!), but if you only want to match a certain group, you can do that too: 484 | 485 | ```yaml 486 | say_hello: 487 | value: hello world! 488 | regex: 489 | matches: (?P.*) world! 490 | group: greeting 491 | ``` 492 | 493 | This will output simply `hello` 494 | 495 | #### JMES Path filter 496 | 497 | You can use [jmespath](http://jmespath.org/) to filter out JSON documents, returning some or more values: 498 | 499 | ```yaml 500 | show_status: 501 | value: "{\"status\": \"ok\"}" 502 | jmespath: status 503 | ``` 504 | 505 | As with regex, this can be part of a filter chain: 506 | 507 | ```yaml 508 | show_status: 509 | value: "{\"status\": \"ok\"}" 510 | filters: 511 | - jmespath: status 512 | ``` 513 | 514 | #### No Output Filter 515 | 516 | If you don't want your output printed in results, you can add no output: 517 | 518 | ```yaml 519 | dont_show_hello: 520 | value: hello 521 | do_output: false 522 | ``` 523 | 524 | You can also add this to a filter chain: 525 | 526 | ```yaml 527 | dont_show_hello: 528 | value: hello 529 | filters: 530 | - nooutput 531 | ``` 532 | 533 | Sometimes you might return too much from a request, so you can use this to ensure what's printed out is not included: 534 | 535 | ```yaml 536 | check_reddit: 537 | http: https://www.reddit.com 538 | filters: 539 | - regex: the front page of the internet 540 | 541 | ``` 542 | 543 | ### Expect types 544 | 545 | There are 3 expect types currently: Match output, Greater than and Less than. The expect types will take the raw output of the step type and validate against that. In this way you can use it to match against the returned HTML from a web server, or the output of a bash file. 546 | 547 | #### Match Expect type 548 | 549 | The match expect type will use regex to match the output of a command. 550 | 551 | ```yaml 552 | say_hello_or_goodbye: 553 | value: hello 554 | matches: hello|goodbye 555 | ``` 556 | 557 | If there is an error converting the regex into a valid regex query, then this will be treated as a failure. 558 | 559 | #### Greater than or less than 560 | 561 | If your output is numerical, then you can use greater than or less than to compare it: 562 | 563 | ```yaml 564 | there_are_four_lights: 565 | value: 4 566 | less_than: 5 567 | ``` 568 | 569 | ### On Fail 570 | 571 | You can run another step when a step fails. This `on_fail` can be any of the step types: bash, http, system, step and value 572 | 573 | ```yaml 574 | on_fail_example: 575 | value: true 576 | matches: false 577 | on_fail: 578 | bash: notify-send "Lorikeet Failed!" 579 | ``` 580 | 581 | The output or error of this on fail step will be included in the standard output. 582 | 583 | If you are using retry counts, then the `on_fail` step will execute each time the step fail. 584 | 585 | 586 | ### Dependencies 587 | 588 | By default tests are run in parallel and submitted to a thread pool for execution. If a step has a dependency it won't be run until the dependent step has been finished. If there are no dependencies to a step then it will run as soon as a thread is free. If you don't specify any dependencies there is no guaranteed ordering to execution. 589 | 590 | Dependencies are important when you need to do things like set cookies before checking API, but will cause your tests to take longer to run while they wait for others to finish. 591 | 592 | To defined dependencies you can use the `require` and `required_by` arguments to control this dependency tree. The required steps are given by their name, and can either be a single value or a list of names: 593 | 594 | ```yaml 595 | step1: 596 | value: hello 597 | 598 | step2: 599 | value: goodbye 600 | require: step1 601 | 602 | step3: 603 | value: yes 604 | require: 605 | - step1 606 | - step2 607 | ``` 608 | 609 | Lorikeet will fail to run and panic if: 610 | 611 | * There is a circular dependency 612 | * The step name in a dependency can't be found 613 | 614 | #### Required By 615 | 616 | `required_by` is just the reciprocal of `require` and can be used where the test plan makes it more readable. 617 | 618 | So this step plan: 619 | 620 | ```yaml 621 | step1: 622 | value: hello 623 | 624 | step2: 625 | value: goodbye 626 | require: step1 627 | ``` 628 | 629 | Is equivalent to this one: 630 | 631 | ```yaml 632 | step1: 633 | value: hello 634 | required_by: step2 635 | 636 | step2: 637 | value: goodbye 638 | ``` 639 | 640 | #### More complex dependency example 641 | 642 | ```yaml 643 | you_say_yes: 644 | value: yes 645 | 646 | i_say_no: 647 | value: no 648 | require: you_say_yes 649 | 650 | you_say_stop: 651 | value: stop 652 | require: 653 | - i_say_no 654 | - you_say_yes 655 | required_by: 656 | - and_i_say_go_go_go 657 | 658 | and_i_say_go_go_go: 659 | value: go go go 660 | ``` 661 | 662 | ### Retry Counts and Delays 663 | 664 | Sometimes you want to delay a step a certain amount of time after another step has been run. Sometimes if a step fails you may also want to retry it a few times before giving up. 665 | 666 | #### Adding a Delay 667 | 668 | You can add a delay by setting the `delay_ms` value: 669 | 670 | ```yaml 671 | step1: 672 | value: hello 673 | delay_ms: 1000 674 | ``` 675 | 676 | Output: 677 | 678 | ```yaml 679 | $ lorikeet test.yml 680 | - name: step1 681 | pass: true 682 | output: hello 683 | duration: 1004.1231ms 684 | ``` 685 | 686 | #### Adding a Retry 687 | 688 | You can retry steps a few times with the `retry_count` and add a delay to the retry by using the `retry_delay_ms`. 689 | 690 | ```yaml 691 | this_will_fail_but_take_3_seconds: 692 | value: hello 693 | matches: goodbye 694 | retry_count: 3 695 | retry_delay_ms: 1000 696 | ``` 697 | 698 | Output: 699 | 700 | ```yaml 701 | $ lorikeet test.yml 702 | - name: this_will_fail_but_take_3_seconds 703 | pass: false 704 | output: hello 705 | error: Not matched against `goodbye` 706 | duration: 3015.933ms 707 | ``` 708 | 709 | ### JUnit Reports 710 | 711 | You can generate a junit xml report with the `-j` command: 712 | 713 | ``` 714 | lorikeet -j report.xml test.yml 715 | ``` 716 | 717 | The output is primarily geared towards using with with [Jenkins BlueOcean](https://jenkins.io/doc/pipeline/tour/tests-and-artifacts/), and the report format may change a little bit. 718 | 719 | ## Examples 720 | 721 | Save these examples as `test.yml` to run them 722 | 723 | ### Echoing `hello` from a bash prompt 724 | 725 | Test Plan: 726 | 727 | ```yaml 728 | say_hello: 729 | bash: echo hello 730 | ``` 731 | 732 | Output: 733 | 734 | ```yaml 735 | $ lorikeet test.yml 736 | - name: say_hello 737 | pass: true 738 | output: | 739 | hello 740 | 741 | duration: 2.727446ms 742 | ``` 743 | 744 | ### Matching the output of a bash command 745 | 746 | Test Plan: 747 | 748 | ```yaml 749 | say_hello: 750 | bash: echo hello 751 | matches: hello 752 | ``` 753 | 754 | Output: 755 | 756 | ```yaml 757 | $ lorikeet test.yml 758 | - name: say_hello 759 | pass: true 760 | duration: 2.68431ms 761 | ``` 762 | 763 | ### Checking whether reddit is down 764 | 765 | Test Plan: 766 | 767 | ```yaml 768 | check_reddit: 769 | http: https://www.reddit.com 770 | matches: the front page of the internet 771 | ``` 772 | 773 | Output: 774 | 775 | ```yaml 776 | $ lorikeet test.yml 777 | - name: say_hello 778 | pass: true 779 | duration: 2.68431ms 780 | ``` 781 | 782 | ### Logging into reddit 783 | 784 | For configuration parameters of tests such as usernames and passwords, it makes sense to separate this out into a different file: 785 | 786 | Config file: 787 | 788 | ```yaml 789 | user: myuser 790 | pass: mypass 791 | ``` 792 | 793 | Test Plan: 794 | 795 | ```yaml 796 | login_to_reddit: 797 | http: 798 | url: https://www.reddit.com/api/login/{{user}} 799 | form: 800 | user: {{user}} 801 | passwd: {{pass}} 802 | api_type: json 803 | ``` 804 | 805 | Output (Don't forget to specify the config file with `-c`) : 806 | 807 | ```yaml 808 | $ lorikeet -c config.yml test.yml 809 | - name: login_to_reddit 810 | pass: true 811 | output: {"json": {"errors": [], "data": {"need_https": true, "modhash": "....", "cookie": "..."}}} 812 | duration: 1420.8466ms 813 | ``` 814 | 815 | 816 | 817 | -------------------------------------------------------------------------------- /lorikeet.svg: -------------------------------------------------------------------------------- 1 | 2 | 21 | 23 | 26 | 30 | 34 | 35 | 38 | 42 | 46 | 47 | 58 | 61 | 65 | 69 | 70 | 81 | 91 | 99 | 103 | 109 | 115 | 119 | 124 | 125 | 129 | 134 | 140 | 145 | 150 | 156 | 157 | 161 | 166 | 172 | 177 | 182 | 188 | 189 | 193 | 198 | 204 | 209 | 214 | 220 | 221 | 231 | 235 | 240 | 246 | 251 | 256 | 262 | 263 | 264 | 295 | 302 | 303 | 305 | 306 | 308 | image/svg+xml 309 | 311 | 312 | 313 | 314 | 315 | 321 | 326 | 335 | 344 | 353 | 362 | 371 | 380 | 389 | 404 | 410 | 426 | 435 | 451 | 467 | 482 | 497 | 502 | 508 | 511 | 517 | 523 | 524 | 525 | 534 | 549 | 564 | 579 | 594 | 603 | 604 | 605 | 606 | -------------------------------------------------------------------------------- /src/graph.rs: -------------------------------------------------------------------------------- 1 | use crate::step::RunType; 2 | use crate::step::Step; 3 | use anyhow::{anyhow, Error}; 4 | use petgraph::prelude::GraphMap; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | #[derive(Debug, PartialEq, Serialize, Deserialize)] 8 | pub struct Require; 9 | 10 | pub fn create_graph(steps: &[Step]) -> Result, Error> { 11 | let mut graph = GraphMap::::new(); 12 | 13 | for i in 0..steps.len() { 14 | //Add a dependency for the step to run first if the run type is `step` 15 | if let RunType::Step(ref dep) = steps[i].run { 16 | let dep_index = steps.iter().position(|step| &step.name == dep).ok_or_else(|| anyhow!("Could not build step graph: `{}` can not be found. defined from step run type on `{}`", dep, steps[i].name))?; 17 | graph.add_edge(dep_index, i, Require); 18 | } 19 | 20 | for dep in steps[i].require.iter() { 21 | let dep_index = steps.iter().position(|step| &step.name == dep).ok_or_else(|| anyhow!("Could not build step graph: `{}` can not be found. defined from `require` on `{}`", dep, steps[i].name))?; 22 | graph.add_edge(dep_index, i, Require); 23 | } 24 | 25 | for dep in steps[i].required_by.iter() { 26 | let dep_index = steps.iter().position(|step| &step.name == dep).ok_or_else(|| anyhow!("Could not build step graph: `{}` can not be found. defined from `required_by` on `{}`", dep, steps[i].name))?; 27 | 28 | graph.add_edge(i, dep_index, Require); 29 | } 30 | } 31 | 32 | match petgraph::algo::toposort(&graph, None) { 33 | Ok(_) => Ok(graph), 34 | Err(err) => { 35 | return Err(anyhow!( 36 | "Could not build step graph: `{}` has a circular dependency", 37 | steps[err.node_id()].name 38 | )); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/junit.rs: -------------------------------------------------------------------------------- 1 | use quick_xml::events::{BytesDecl, BytesEnd, BytesStart, BytesText, Event}; 2 | use quick_xml::Writer; 3 | use std::fs::File; 4 | use std::path::Path; 5 | 6 | use crate::submitter::StepResult; 7 | 8 | use anyhow::Error; 9 | use std::fs::create_dir_all; 10 | 11 | pub fn create_junit( 12 | results: &[StepResult], 13 | file_path: &Path, 14 | hostname: Option<&str>, 15 | ) -> Result<(), Error> { 16 | if let Some(parent) = file_path.parent() { 17 | create_dir_all(parent)?; 18 | } 19 | 20 | let file = File::create(file_path)?; 21 | 22 | let mut writer = Writer::new_with_indent(file, b' ', 4); 23 | 24 | writer.write_event(Event::Decl(BytesDecl::new(b"1.0", Some(b"UTF-8"), None)))?; 25 | 26 | // Add in the testsuite elem 27 | 28 | let test_num = results.len(); 29 | let skip_num = results 30 | .iter() 31 | .filter(|step| { 32 | if let Some(ref output) = step.error { 33 | return output == "Dependency Not Met"; 34 | } 35 | false 36 | }) 37 | .count(); 38 | let failure_num = results.iter().filter(|step| !step.pass).count() - skip_num; 39 | 40 | let time = results 41 | .iter() 42 | .fold(0f32, |sum, step| sum + (step.duration / 1000f32)); 43 | 44 | let hostname = match hostname { 45 | Some(hostname) => String::from(hostname), 46 | None => hostname::get() 47 | .map(|name| name.to_string_lossy().to_string()) 48 | .unwrap_or_else(|_| String::from("")), 49 | }; 50 | 51 | let mut testsuite = BytesStart::borrowed(b"testsuite", b"testsuite".len()); 52 | 53 | testsuite.push_attribute(("name", "lorikeet")); 54 | testsuite.push_attribute(("hostname", &*hostname)); 55 | 56 | testsuite.push_attribute(("tests", &*test_num.to_string())); 57 | testsuite.push_attribute(("failures", &*failure_num.to_string())); 58 | testsuite.push_attribute(("skipped", &*skip_num.to_string())); 59 | testsuite.push_attribute(("time", &*time.to_string())); 60 | 61 | writer.write_event(Event::Start(testsuite))?; 62 | 63 | for result in results.iter() { 64 | let mut testcase = BytesStart::borrowed(b"testcase", b"testcase".len()); 65 | 66 | testcase.push_attribute(("name", &*result.name)); 67 | 68 | if let Some(ref desc) = result.description { 69 | testcase.push_attribute(("classname", desc as &str)); 70 | } else { 71 | testcase.push_attribute(("classname", "")); 72 | } 73 | 74 | testcase.push_attribute(("time", &*(result.duration / 1000f32).to_string())); 75 | 76 | writer.write_event(Event::Start(testcase))?; 77 | 78 | writer.write_event(Event::Start(BytesStart::borrowed( 79 | b"system-out", 80 | b"system-out".len(), 81 | )))?; 82 | 83 | writer.write_event(Event::Text(BytesText::from_plain_str( 84 | &filter_invalid_chars(&result.output), 85 | )))?; 86 | 87 | writer.write_event(Event::End(BytesEnd::borrowed(b"system-out")))?; 88 | 89 | if !result.pass { 90 | let error_text = result.error.as_deref().unwrap_or(""); 91 | 92 | if error_text == "Dependency Not Met" { 93 | let mut skipped = BytesStart::borrowed(b"skipped", b"skipped".len()); 94 | skipped.push_attribute(("message", "Dependency Not Met")); 95 | 96 | writer.write_event(Event::Start(skipped))?; 97 | 98 | writer.write_event(Event::End(BytesEnd::borrowed(b"skipped")))?; 99 | } else { 100 | let mut failure = BytesStart::borrowed(b"failure", b"failure".len()); 101 | failure.push_attribute(("message", "Step failed to finish")); 102 | 103 | writer.write_event(Event::Start(failure))?; 104 | writer.write_event(Event::Text(BytesText::from_plain_str( 105 | &filter_invalid_chars(error_text), 106 | )))?; 107 | writer.write_event(Event::End(BytesEnd::borrowed(b"failure")))?; 108 | } 109 | } 110 | 111 | writer.write_event(Event::End(BytesEnd::borrowed(b"testcase")))?; 112 | } 113 | 114 | writer.write_event(Event::End(BytesEnd::borrowed(b"testsuite")))?; 115 | 116 | Ok(()) 117 | } 118 | 119 | fn filter_invalid_chars(input: &str) -> String { 120 | let mut output = String::new(); 121 | 122 | for ch in input.chars() { 123 | if ('\u{0020}'..='\u{D7FF}').contains(&ch) 124 | || ('\u{E000}'..='\u{FFFD}').contains(&ch) 125 | || ch == '\u{0009}' 126 | || ch == '\u{0A}' 127 | || ch == '\u{0D}' 128 | { 129 | output.push(ch); 130 | } 131 | } 132 | 133 | output 134 | } 135 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod graph; 2 | pub mod junit; 3 | pub mod runner; 4 | pub mod step; 5 | pub mod submitter; 6 | pub mod yaml; 7 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use futures::StreamExt; 2 | use structopt::StructOpt; 3 | 4 | use std::path::{Path, PathBuf}; 5 | 6 | use anyhow::Error; 7 | 8 | use log::{debug, trace}; 9 | 10 | use lorikeet::runner::run_steps; 11 | use lorikeet::step::{ExpectType, Outcome, RetryPolicy, RunType, Step}; 12 | use lorikeet::submitter::StepResult; 13 | use lorikeet::yaml::get_steps; 14 | 15 | use std::time::Duration; 16 | 17 | #[derive(StructOpt, Debug)] 18 | #[structopt(name = "lorikeet", about = "a parallel test runner for devops")] 19 | struct Arguments { 20 | #[structopt(short = "q", long = "quiet", help = "Don't output results to console")] 21 | quiet: bool, 22 | 23 | #[structopt(short = "c", long = "config", help = "Configuration File")] 24 | config: Option, 25 | 26 | #[structopt(short = "h", long = "hostname", help = "Hostname")] 27 | hostname: Option, 28 | 29 | #[structopt(short = "t", long = "terminal", help = "Force terminal colours")] 30 | term: bool, 31 | 32 | #[structopt(help = "Test Plan", default_value = "test.yml")] 33 | test_plan: String, 34 | 35 | #[structopt( 36 | short = "w", 37 | long = "webhook", 38 | help = "Webhook submission URL (multiple values allowed)" 39 | )] 40 | webhook: Vec, 41 | 42 | #[structopt( 43 | short = "s", 44 | long = "slack", 45 | help = "Slack Webhook submission URL (multiple values allowed)" 46 | )] 47 | slack: Vec, 48 | 49 | #[structopt( 50 | short = "j", 51 | long = "junit", 52 | help = "Output a JUnit XML Report to this file", 53 | parse(from_os_str) 54 | )] 55 | junit: Option, 56 | } 57 | 58 | #[tokio::main] 59 | async fn main() { 60 | let opt = Arguments::from_args(); 61 | 62 | env_logger::init(); 63 | 64 | debug!("Loading Steps from `{}`", opt.test_plan); 65 | 66 | let colours = atty::is(atty::Stream::Stdout) || opt.term; 67 | 68 | let results = run_steps_or_error(&opt.test_plan, &opt.config, opt.quiet, colours).await; 69 | 70 | let has_errors = results.iter().any(|val| !val.pass); 71 | 72 | debug!("Steps finished!"); 73 | 74 | if !opt.webhook.is_empty() { 75 | let hostname = opt.hostname.clone().unwrap_or_else(|| { 76 | hostname::get() 77 | .map(|val| val.to_string_lossy().to_string()) 78 | .unwrap_or_else(|_| "".into()) 79 | }); 80 | 81 | for url in opt.webhook { 82 | debug!("Sending webhook to: {}", url); 83 | lorikeet::submitter::submit_webhook(&results, &url, &hostname) 84 | .await 85 | .expect("Could not send webhook") 86 | } 87 | } 88 | 89 | if !opt.slack.is_empty() { 90 | let hostname = opt.hostname.unwrap_or_else(|| { 91 | hostname::get() 92 | .map(|val| val.to_string_lossy().to_string()) 93 | .unwrap_or_else(|_| "".into()) 94 | }); 95 | 96 | for url in opt.slack { 97 | debug!("Sending slack webhook to: {}", url); 98 | lorikeet::submitter::submit_slack(&results, &url, &hostname) 99 | .await 100 | .expect("Could not send webhook") 101 | } 102 | } 103 | 104 | if let Some(path) = opt.junit { 105 | debug!("Creating junit file at `{}`", path.display()); 106 | lorikeet::junit::create_junit(&results, &path, None).expect("Coult not create junit file"); 107 | } 108 | 109 | if has_errors { 110 | std::process::exit(1) 111 | } 112 | } 113 | 114 | // Runs the steps, or if there is an issue running the steps, then return the error as a step 115 | async fn run_steps_or_error, Q: AsRef>( 116 | file_path: P, 117 | config_path: &Option, 118 | quiet: bool, 119 | colours: bool, 120 | ) -> Vec { 121 | let steps = match get_steps(file_path, config_path) { 122 | Ok(steps) => steps, 123 | Err(err) => return vec![step_from_error(err, quiet, colours)], 124 | }; 125 | 126 | trace!("Steps:{:?}", steps); 127 | 128 | match run_steps(steps) { 129 | Ok(mut stream) => { 130 | let mut results = Vec::new(); 131 | 132 | while let Some(step) = stream.next().await { 133 | let result: StepResult = step.into(); 134 | 135 | if !quiet { 136 | result.terminal_print(&colours); 137 | } 138 | 139 | results.push(result); 140 | } 141 | 142 | results 143 | } 144 | Err(err) => vec![step_from_error(err, quiet, colours)], 145 | } 146 | } 147 | 148 | fn step_from_error(err: Error, quiet: bool, colours: bool) -> StepResult { 149 | let outcome = Outcome { 150 | output: None, 151 | error: Some(err.to_string()), 152 | duration: Duration::default(), 153 | on_fail_output: None, 154 | on_fail_error: None, 155 | }; 156 | 157 | let result: StepResult = Step { 158 | name: "lorikeet".into(), 159 | run: RunType::Value(String::new()), 160 | do_output: true, 161 | expect: ExpectType::Anything, 162 | on_fail: None, 163 | description: Some( 164 | "This step is shown if there was an error when reading, parsing or running steps" 165 | .into(), 166 | ), 167 | filters: vec![], 168 | require: vec![], 169 | required_by: vec![], 170 | retry: RetryPolicy::default(), 171 | outcome: Some(outcome), 172 | } 173 | .into(); 174 | 175 | if !quiet { 176 | result.terminal_print(&colours); 177 | } 178 | 179 | result 180 | } 181 | -------------------------------------------------------------------------------- /src/runner.rs: -------------------------------------------------------------------------------- 1 | use crate::step::FilterType; 2 | 3 | use futures::stream::Stream; 4 | use std::collections::HashMap; 5 | use std::pin::Pin; 6 | use std::task::{Context, Poll}; 7 | use std::time::Duration; 8 | use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; 9 | 10 | use crate::step::{ExpectType, Outcome, RetryPolicy, RunType, Step, STEP_OUTPUT}; 11 | 12 | use crate::graph::{create_graph, Require}; 13 | use petgraph::prelude::GraphMap; 14 | use petgraph::{Directed, Direction}; 15 | 16 | use log::*; 17 | 18 | use anyhow::Error; 19 | 20 | pub struct StepRunner { 21 | pub name: String, 22 | pub index: usize, 23 | pub run: RunType, 24 | pub on_fail: Option, 25 | pub expect: ExpectType, 26 | pub retry: RetryPolicy, 27 | pub filters: Vec, 28 | pub notify: UnboundedSender<(usize, Outcome)>, 29 | } 30 | 31 | //Spawns into a background task so we can poll the rest 32 | impl StepRunner { 33 | pub fn poll(self) { 34 | debug!("Running: {}", self.name); 35 | 36 | tokio::spawn(async move { 37 | let outcome = self 38 | .run 39 | .execute(self.expect, self.filters, self.retry, self.on_fail) 40 | .await; 41 | 42 | if let Some(ref output) = outcome.output { 43 | STEP_OUTPUT.insert(self.name.clone(), output.clone()); 44 | } 45 | 46 | if let Err(err) = self.notify.send((self.index, outcome)) { 47 | error!("Could not notify executor:{}", err); 48 | } 49 | 50 | debug!("Completed: {}", self.name); 51 | }); 52 | } 53 | } 54 | 55 | #[derive(Clone, Debug, PartialEq)] 56 | enum Status { 57 | Awaiting, 58 | Completed, 59 | Error, 60 | } 61 | 62 | pub struct StepStream { 63 | channel: UnboundedReceiver, 64 | } 65 | 66 | impl Stream for StepStream { 67 | type Item = Step; 68 | 69 | fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 70 | self.channel.poll_recv(cx) 71 | } 72 | } 73 | 74 | pub fn run_steps(steps: Vec) -> Result { 75 | let graph = create_graph(&steps)?; 76 | 77 | let mut step_map = steps.into_iter().enumerate().collect::>(); 78 | 79 | let (tx_steps, rx_steps) = unbounded_channel(); 80 | 81 | let step_stream = StepStream { channel: rx_steps }; 82 | 83 | tokio::spawn(async move { 84 | let mut statuses = Vec::new(); 85 | statuses.resize(step_map.len(), Status::Awaiting); 86 | 87 | //We want the runners to drop after this so we can return the steps status 88 | { 89 | let mut runners = Vec::new(); 90 | 91 | let (tx, mut rx) = unbounded_channel(); 92 | 93 | for (i, step) in step_map.iter() { 94 | let future = StepRunner { 95 | run: step.run.clone(), 96 | on_fail: step.on_fail.clone(), 97 | expect: step.expect.clone(), 98 | retry: step.retry, 99 | filters: step.filters.clone(), 100 | name: step.name.clone(), 101 | index: *i, 102 | notify: tx.clone(), 103 | }; 104 | 105 | runners.push(future); 106 | } 107 | 108 | //We want to start all the ones that don't have any outgoing neighbors 109 | let (to_start, waiting) = runners 110 | .into_iter() 111 | .partition::, _>(|job| can_start(job.index, &statuses, &graph)); 112 | 113 | runners = waiting; 114 | 115 | let mut active = 0; 116 | 117 | for runner in to_start.into_iter() { 118 | runner.poll(); 119 | active += 1; 120 | } 121 | 122 | while active > 0 { 123 | debug!( 124 | "Active amount: {}, runners waiting: {}", 125 | active, 126 | runners.len() 127 | ); 128 | if let Some((idx, outcome)) = rx.recv().await { 129 | active -= 1; 130 | let has_error = outcome.error.is_some(); 131 | 132 | statuses[idx] = if has_error { 133 | Status::Error 134 | } else { 135 | Status::Completed 136 | }; 137 | 138 | if let Some(mut step) = step_map.remove(&idx) { 139 | step.outcome = Some(outcome); 140 | if tx_steps.send(step).is_err() { 141 | error!("Error sending step!"); 142 | } 143 | } 144 | 145 | for neighbor in graph.neighbors_directed(idx, Direction::Outgoing) { 146 | if let Some(job_idx) = runners.iter().position(|job| job.index == neighbor) 147 | { 148 | if !has_error && can_start(runners[job_idx].index, &statuses, &graph) { 149 | let runner = runners.swap_remove(job_idx); 150 | runner.poll(); 151 | active += 1; 152 | } 153 | } 154 | } 155 | } 156 | } 157 | } 158 | 159 | for (i, _status) in statuses.into_iter().enumerate() { 160 | if let Some(mut step) = step_map.remove(&i) { 161 | step.outcome = Some(Outcome { 162 | output: Some("".into()), 163 | error: Some("Dependency Not Met".into()), 164 | duration: Duration::from_secs(0), 165 | on_fail_output: None, 166 | on_fail_error: None, 167 | }); 168 | 169 | if tx_steps.send(step).is_err() { 170 | error!("Error sending step!"); 171 | } 172 | } 173 | } 174 | }); 175 | 176 | Ok(step_stream) 177 | } 178 | 179 | fn can_start(idx: usize, statuses: &[Status], graph: &GraphMap) -> bool { 180 | debug!("Checking if we can start for {}", idx); 181 | 182 | for neighbor in graph.neighbors_directed(idx, Direction::Incoming) { 183 | match statuses[neighbor] { 184 | Status::Awaiting => { 185 | debug!("Neighbour {} Not Completed", neighbor); 186 | return false; 187 | } 188 | Status::Completed => { 189 | debug!("Neighbour {} Completed", neighbor); 190 | } 191 | Status::Error => { 192 | debug!("Neighbour {} Has Error", neighbor); 193 | return false; 194 | } 195 | } 196 | } 197 | 198 | true 199 | } 200 | -------------------------------------------------------------------------------- /src/step/bash.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 4 | #[serde(untagged)] 5 | pub enum BashVariant { 6 | CmdOnly(String), 7 | Options(BashOptions), 8 | } 9 | 10 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 11 | pub struct BashOptions { 12 | cmd: String, 13 | full_error: bool, 14 | } 15 | 16 | use std::process::Command; 17 | 18 | use super::output_renderer; 19 | 20 | impl BashVariant { 21 | pub async fn run(&self) -> Result { 22 | let bashopts = match *self { 23 | BashVariant::CmdOnly(ref val) => BashOptions { 24 | cmd: val.clone(), 25 | full_error: false, 26 | }, 27 | BashVariant::Options(ref opts) => opts.clone(), 28 | }; 29 | 30 | tokio::task::spawn_blocking(move || { 31 | let cmd = output_renderer(&bashopts.cmd)?; 32 | 33 | match Command::new("bash").arg("-c").arg(cmd).output() { 34 | Ok(output) => { 35 | if output.status.success() { 36 | Ok(format!("{}", String::from_utf8_lossy(&output.stdout))) 37 | } else if bashopts.full_error { 38 | Err(format!( 39 | "Status Code:{}\nError:{}\nOutput:{}", 40 | output.status.code().unwrap_or(1), 41 | String::from_utf8_lossy(&output.stderr), 42 | String::from_utf8_lossy(&output.stdout) 43 | )) 44 | } else { 45 | Err(String::from_utf8_lossy(&output.stderr).to_string()) 46 | } 47 | } 48 | Err(err) => Err(format!("Err:{:?}", err)), 49 | } 50 | }) 51 | .await 52 | .map_err(|err| format!("{}", err))? 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/step/disk.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | #[cfg(not(target_os = "windows"))] 3 | use std::cmp; 4 | 5 | #[cfg(not(target_os = "windows"))] 6 | use log::*; 7 | #[cfg(not(target_os = "windows"))] 8 | use std::{ffi::CString, mem::zeroed}; 9 | 10 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 11 | #[serde(untagged)] 12 | pub enum DiskVariant { 13 | MountPointOnly(String), 14 | Options(DiskOptions), 15 | } 16 | 17 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 18 | pub struct DiskOptions { 19 | mount: String, 20 | #[serde(default, rename = "type")] 21 | disk_type: DiskType, 22 | #[serde(default)] 23 | output_type: OutputType, 24 | } 25 | 26 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 27 | #[serde(rename_all = "snake_case")] 28 | pub enum DiskType { 29 | Size, 30 | Used, 31 | Free, 32 | } 33 | 34 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 35 | #[serde(rename_all = "snake_case")] 36 | pub enum OutputType { 37 | Bytes, 38 | Human, 39 | Percent, 40 | } 41 | 42 | impl Default for DiskType { 43 | fn default() -> Self { 44 | DiskType::Free 45 | } 46 | } 47 | 48 | impl Default for OutputType { 49 | fn default() -> Self { 50 | OutputType::Bytes 51 | } 52 | } 53 | 54 | impl DiskVariant { 55 | pub async fn run(&self) -> Result { 56 | let diskops = match *self { 57 | DiskVariant::MountPointOnly(ref mount) => DiskOptions { 58 | mount: mount.clone(), 59 | disk_type: DiskType::Free, 60 | output_type: OutputType::Bytes, 61 | }, 62 | DiskVariant::Options(ref ops) => ops.clone(), 63 | }; 64 | 65 | let stavfs = get_stats(&diskops)?; 66 | 67 | Ok(stavfs) 68 | } 69 | } 70 | 71 | #[cfg(not(target_os = "windows"))] 72 | pub fn get_stats(ops: &DiskOptions) -> Result { 73 | let mountp = CString::new(ops.mount.clone()).unwrap(); 74 | let mnt_ptr = mountp.as_ptr(); 75 | 76 | let stats = unsafe { 77 | let mut stats: libc::statvfs = zeroed(); 78 | if libc::statvfs(mnt_ptr, &mut stats) != 0 { 79 | return Err(format!( 80 | "Unable to retrive stats of {}: {}", 81 | ops.mount, 82 | std::io::Error::last_os_error() 83 | )); 84 | } 85 | 86 | stats 87 | }; 88 | 89 | debug!( 90 | "f_blocks:{}, f_bsize:{}, f_frsize:{}, f_bavail:{}, f_bfree:{}", 91 | stats.f_blocks, stats.f_bsize, stats.f_frsize, stats.f_bavail, stats.f_bfree 92 | ); 93 | 94 | let size = stats.f_blocks as usize * stats.f_frsize as usize; 95 | let free = stats.f_bavail as usize * stats.f_frsize as usize; 96 | let used = size - free; 97 | 98 | debug!("size: {}, free:{}, used:{}", size, free, used); 99 | 100 | let output = match ops.disk_type { 101 | DiskType::Size => size, 102 | DiskType::Used => used, 103 | DiskType::Free => free, 104 | }; 105 | 106 | match ops.output_type { 107 | OutputType::Bytes => Ok(output.to_string()), 108 | OutputType::Percent => { 109 | if size == 0 { 110 | return Err(format!( 111 | "Size for mount `{}` is 0. Can't create percentage", 112 | ops.mount 113 | )); 114 | } 115 | return Ok(format!( 116 | "{}%", 117 | ((output as f64 / size as f64) * 100.0).round() as usize 118 | )); 119 | } 120 | OutputType::Human => Ok(pretty_bytes(output as f64)), 121 | } 122 | } 123 | 124 | #[cfg(target_os = "windows")] 125 | pub fn get_stats(_ops: &DiskOptions) -> Result { 126 | return Err("Not Implemented Yet".into()); 127 | } 128 | 129 | #[cfg(not(target_os = "windows"))] 130 | pub fn pretty_bytes(num: f64) -> String { 131 | let negative = if num.is_sign_positive() { "" } else { "-" }; 132 | let num = num.abs(); 133 | let units = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; 134 | if num < 1_f64 { 135 | return format!("{}{} {}", negative, num, "B"); 136 | } 137 | let delimiter = 1000_f64; 138 | let exponent = cmp::min( 139 | (num.ln() / delimiter.ln()).floor() as i32, 140 | (units.len() - 1) as i32, 141 | ); 142 | 143 | let unit = units[exponent as usize]; 144 | return format!("{}{:.2}{}", negative, num / delimiter.powi(exponent), unit); 145 | } 146 | -------------------------------------------------------------------------------- /src/step/http.rs: -------------------------------------------------------------------------------- 1 | use crate::step::output_renderer; 2 | 3 | use super::STEP_OUTPUT; 4 | use regex::Regex; 5 | use serde::{Deserialize, Deserializer, Serialize, Serializer}; 6 | 7 | use reqwest::{ 8 | header::{HeaderValue, COOKIE, SET_COOKIE}, 9 | multipart::Form, 10 | multipart::Part, 11 | redirect::Policy, 12 | Body, Method, 13 | }; 14 | 15 | use tokio::fs::File; 16 | 17 | use chashmap::CHashMap; 18 | use lazy_static::lazy_static; 19 | 20 | use cookie::{Cookie, CookieJar}; 21 | 22 | use tokio_util::codec::{BytesCodec, FramedRead}; 23 | 24 | use std::{collections::HashMap, time::Duration}; 25 | use std::{path::PathBuf, str::FromStr}; 26 | 27 | lazy_static! { 28 | static ref COOKIES: CHashMap = CHashMap::new(); 29 | static ref REGEX_OUTPUT: Regex = Regex::new("\\$\\{(step_output.[^}]+)\\}").unwrap(); 30 | } 31 | 32 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 33 | #[serde(untagged)] 34 | pub enum HttpVariant { 35 | UrlOnly(String), 36 | Options(Box), 37 | } 38 | 39 | fn method_to_string(method: &Method, s: S) -> Result 40 | where 41 | S: Serializer, 42 | { 43 | s.serialize_str(method.as_ref()) 44 | } 45 | 46 | fn string_to_method<'de, D>(d: D) -> Result 47 | where 48 | D: Deserializer<'de>, 49 | { 50 | Deserialize::deserialize(d) 51 | .and_then(|val: String| Method::from_str(&val).map_err(serde::de::Error::custom)) 52 | } 53 | 54 | fn default_cookies() -> bool { 55 | true 56 | } 57 | 58 | fn default_status() -> u16 { 59 | 200 60 | } 61 | 62 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 63 | pub struct HttpOptions { 64 | url: String, 65 | #[serde( 66 | default, 67 | deserialize_with = "string_to_method", 68 | serialize_with = "method_to_string" 69 | )] 70 | method: Method, 71 | #[serde(default = "default_cookies")] 72 | save_cookies: bool, 73 | #[serde(default = "default_status")] 74 | status: u16, 75 | #[serde(default)] 76 | headers: Option>, 77 | #[serde(default)] 78 | user: Option, 79 | #[serde(default)] 80 | body: Option, 81 | #[serde(default)] 82 | pass: Option, 83 | #[serde(default)] 84 | form: Option>, 85 | #[serde(default)] 86 | multipart: Option>, 87 | #[serde(default = "default_timeout")] 88 | timeout_ms: Option, 89 | #[serde(default)] 90 | verify_ssl: Option, 91 | } 92 | 93 | fn default_timeout() -> Option { 94 | Some(30000) 95 | } 96 | 97 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 98 | #[serde(untagged)] 99 | pub enum MultipartValue { 100 | Value(String), 101 | Path(PathStruct), 102 | Step(StepStruct), 103 | } 104 | 105 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 106 | pub struct PathStruct { 107 | file: PathBuf, 108 | } 109 | 110 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 111 | pub struct StepStruct { 112 | step: String, 113 | } 114 | 115 | impl HttpVariant { 116 | pub async fn run(&self) -> Result { 117 | let mut httpops = match *self { 118 | HttpVariant::UrlOnly(ref val) => HttpOptions { 119 | url: val.clone(), 120 | method: Method::GET, 121 | status: default_status(), 122 | headers: None, 123 | save_cookies: default_cookies(), 124 | user: None, 125 | pass: None, 126 | body: None, 127 | form: None, 128 | multipart: None, 129 | timeout_ms: default_timeout(), 130 | verify_ssl: None, 131 | }, 132 | HttpVariant::Options(ref opts) => *opts.clone(), 133 | }; 134 | 135 | let mut client_builder = reqwest::ClientBuilder::new().redirect(Policy::none()); 136 | 137 | if let Some(timeout) = httpops.timeout_ms { 138 | client_builder = client_builder.timeout(Duration::from_millis(timeout)); 139 | } 140 | 141 | if let Some(verify_ssl) = httpops.verify_ssl { 142 | client_builder = client_builder.danger_accept_invalid_certs(!verify_ssl); 143 | } 144 | 145 | let client = client_builder.build().map_err(|err| format!("{}", err))?; 146 | 147 | let url = reqwest::Url::from_str(&httpops.url) 148 | .map_err(|err| format!("Failed to parse url `{}`: {}", httpops.url, err))?; 149 | 150 | let hostname: String = url 151 | .host_str() 152 | .map(String::from) 153 | .ok_or_else(|| format!("No host could be found for url: {}", url))?; 154 | 155 | if (httpops.form.is_some() || httpops.multipart.is_some() || httpops.body.is_some()) 156 | && httpops.method == Method::GET 157 | { 158 | httpops.method = Method::POST; 159 | } 160 | 161 | let mut request = client.request(httpops.method, url); 162 | 163 | if let Some(user) = httpops.user { 164 | request = request.basic_auth(user, httpops.pass) 165 | } 166 | 167 | if let Some(form) = httpops.form { 168 | request = request.form(&form) 169 | } 170 | 171 | if let Some(multipart) = httpops.multipart { 172 | let mut form = Form::new(); 173 | 174 | for (key, val) in multipart.into_iter() { 175 | form = match val { 176 | MultipartValue::Value(string) => form.text(key, string), 177 | MultipartValue::Path(path_struct) => { 178 | let file_name = path_struct 179 | .file 180 | .file_name() 181 | .map(|val| val.to_string_lossy().to_string()) 182 | .unwrap_or_default(); 183 | 184 | let file = File::open(&path_struct.file) 185 | .await 186 | .map_err(|err| format!("{:?}", err))?; 187 | let reader = Body::wrap_stream(FramedRead::new(file, BytesCodec::new())); 188 | form.part(key, Part::stream(reader).file_name(file_name)) 189 | } 190 | MultipartValue::Step(step) => match STEP_OUTPUT.get(&step.step) { 191 | Some(val) => form.text(key, val.to_string()), 192 | None => return Err(format!("Step {} could not be found", &step.step)), 193 | }, 194 | } 195 | } 196 | 197 | request = request.multipart(form) 198 | } 199 | 200 | if let Some(body) = httpops.body { 201 | request = request.body(output_renderer(&body)?); 202 | } 203 | 204 | if let Some(cookie_jar) = COOKIES.get(&hostname) { 205 | let cookie_strings: Vec = cookie_jar.iter().map(Cookie::to_string).collect(); 206 | request = request.header(COOKIE, cookie_strings.join("; ")) 207 | } 208 | 209 | if let Some(headers) = httpops.headers { 210 | for (key, val) in headers.into_iter() { 211 | request = request.header(&*key, &*val); 212 | } 213 | } 214 | 215 | let response = client 216 | .execute(request.build().map_err(|err| format!("{:?}", err))?) 217 | .await 218 | .map_err(|err| format!("Error connecting to url {}", err))?; 219 | 220 | if response.status().as_u16() != httpops.status { 221 | return Err(format!( 222 | "returned status `{}` does not match expected `{}`", 223 | response.status().as_u16(), 224 | httpops.status 225 | )); 226 | } 227 | 228 | if httpops.save_cookies { 229 | let new_cookies = response.headers().get_all(SET_COOKIE); 230 | 231 | COOKIES.alter(hostname, |value| { 232 | let mut cookie_jar = value.unwrap_or_default(); 233 | for cookie in new_cookies 234 | .iter() 235 | .flat_map(HeaderValue::to_str) 236 | .map(String::from) 237 | .flat_map(Cookie::parse) 238 | { 239 | cookie_jar.add(cookie); 240 | } 241 | Some(cookie_jar) 242 | }); 243 | } 244 | 245 | let output = response.text().await.map_err(|err| format!("{:?}", err))?; 246 | 247 | Ok(output) 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /src/step/mod.rs: -------------------------------------------------------------------------------- 1 | mod bash; 2 | mod disk; 3 | mod http; 4 | mod system; 5 | 6 | pub use bash::BashVariant; 7 | pub use disk::DiskVariant; 8 | pub use http::HttpVariant; 9 | pub use system::SystemVariant; 10 | 11 | use regex::Regex; 12 | 13 | use serde::{Deserialize, Serialize}; 14 | use std::time::{Duration, Instant}; 15 | use tokio::time::sleep; 16 | 17 | use tera::{Context, Tera}; 18 | 19 | use std::{borrow::Cow, collections::HashMap}; 20 | 21 | use jmespath::{self, Variable}; 22 | 23 | use lazy_static::lazy_static; 24 | use log::debug; 25 | 26 | use chashmap::CHashMap; 27 | 28 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 29 | pub struct Outcome { 30 | pub output: Option, 31 | pub error: Option, 32 | pub on_fail_output: Option, 33 | pub on_fail_error: Option, 34 | pub duration: Duration, 35 | } 36 | 37 | #[derive(Default, Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] 38 | pub struct RetryPolicy { 39 | pub retry_count: usize, 40 | pub retry_delay_ms: usize, 41 | pub initial_delay_ms: usize, 42 | } 43 | 44 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 45 | pub struct Step { 46 | pub name: String, 47 | pub description: Option, 48 | pub run: RunType, 49 | pub on_fail: Option, 50 | pub filters: Vec, 51 | pub expect: ExpectType, 52 | pub do_output: bool, 53 | pub outcome: Option, 54 | pub retry: RetryPolicy, 55 | pub require: Vec, 56 | pub required_by: Vec, 57 | } 58 | 59 | #[derive(Debug, PartialEq, Deserialize)] 60 | #[serde(untagged)] 61 | pub enum Requirement { 62 | Some(String), 63 | Many(Vec), 64 | } 65 | 66 | impl Requirement { 67 | pub fn to_vec(&self) -> Vec { 68 | match *self { 69 | Requirement::Some(ref string) => vec![string.clone()], 70 | Requirement::Many(ref vec) => vec.clone(), 71 | } 72 | } 73 | } 74 | 75 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 76 | #[serde(rename_all = "lowercase")] 77 | pub enum RunType { 78 | Step(String), 79 | Value(String), 80 | Bash(BashVariant), 81 | Http(HttpVariant), 82 | System(SystemVariant), 83 | Disk(DiskVariant), 84 | } 85 | 86 | lazy_static! { 87 | pub static ref STEP_OUTPUT: CHashMap = CHashMap::new(); 88 | static ref REGEX_OUTPUT: Regex = Regex::new("\\$\\{(step_output.[^}]+)\\}").unwrap(); 89 | } 90 | 91 | impl RunType { 92 | pub async fn execute( 93 | &self, 94 | expect: ExpectType, 95 | filters: Vec, 96 | retry: RetryPolicy, 97 | on_fail: Option, 98 | ) -> Outcome { 99 | let start = Instant::now(); 100 | 101 | if retry.initial_delay_ms > 0 { 102 | debug!("Initially Sleeping for {} ms", retry.initial_delay_ms); 103 | let delay = Duration::from_millis(retry.initial_delay_ms as u64); 104 | sleep(delay).await; 105 | } 106 | 107 | let try_count = retry.retry_count + 1; 108 | 109 | let mut output = String::new(); 110 | let mut error = String::new(); 111 | let mut on_fail_output = None; 112 | let mut on_fail_error = None; 113 | let mut successful = false; 114 | 115 | 'retry: for count in 0..try_count { 116 | //If this is a retry, sleep first before trying again 117 | if count > 0 { 118 | debug!("Retry {} of {}", count + 1, try_count - 1); 119 | 120 | if retry.retry_delay_ms > 0 { 121 | debug!("Sleeping for {} ms", retry.retry_delay_ms); 122 | let delay = Duration::from_millis(retry.retry_delay_ms as u64); 123 | sleep(delay).await; 124 | } 125 | } 126 | 127 | output = String::new(); 128 | error = String::new(); 129 | on_fail_output = None; 130 | on_fail_error = None; 131 | 132 | //Run the runner first 133 | match self.run().await { 134 | Ok(run_out) => { 135 | output = run_out; 136 | successful = true; 137 | } 138 | Err(run_err) => { 139 | error = run_err; 140 | successful = false; 141 | } 142 | } 143 | 144 | //If it's successful, run the filters, changing the output each iteration 145 | if successful { 146 | 'filter: for filter in filters.iter() { 147 | match filter.filter(&output) { 148 | Ok(filter_out) => { 149 | output = filter_out; 150 | } 151 | Err(filter_err) => { 152 | error = filter_err; 153 | successful = false; 154 | break 'filter; 155 | } 156 | }; 157 | } 158 | } 159 | 160 | //If it's still successful, do the check 161 | if successful { 162 | if let Err(check_err) = expect.check(&output) { 163 | error = check_err; 164 | successful = false; 165 | } else { 166 | break 'retry; 167 | } 168 | } 169 | 170 | if !successful { 171 | if let Some(ref on_fail_runner) = on_fail { 172 | match on_fail_runner.run().await { 173 | Ok(val) => { 174 | on_fail_output = Some(val); 175 | } 176 | Err(val) => on_fail_error = Some(val), 177 | } 178 | } 179 | } 180 | } 181 | 182 | let output_opt = match output.as_ref() { 183 | "" => None, 184 | _ => Some(output), 185 | }; 186 | 187 | let error_opt = match successful { 188 | true => None, 189 | false => Some(error), 190 | }; 191 | 192 | //Default Return 193 | Outcome { 194 | output: output_opt, 195 | error: error_opt, 196 | duration: start.elapsed(), 197 | on_fail_output, 198 | on_fail_error, 199 | } 200 | } 201 | 202 | async fn run(&self) -> Result { 203 | match *self { 204 | RunType::Step(ref val) => match STEP_OUTPUT.get(val) { 205 | Some(val) => Ok(val.to_string()), 206 | None => return Err(format!("Step {} could not be found", val)), 207 | }, 208 | RunType::Value(ref val) => Ok(val.clone()), 209 | RunType::Bash(ref val) => val.run().await, 210 | RunType::Http(ref val) => val.run().await, 211 | RunType::System(ref val) => val.run().await, 212 | RunType::Disk(ref val) => val.run().await, 213 | } 214 | } 215 | } 216 | 217 | impl Step { 218 | pub fn get_duration_ms(&self) -> f32 { 219 | match self.outcome { 220 | Some(ref outcome) => { 221 | let nanos = outcome.duration.subsec_nanos() as f32; 222 | (1000000000f32 * outcome.duration.as_secs() as f32 + nanos) / (1000000f32) 223 | } 224 | None => 0f32, 225 | } 226 | } 227 | } 228 | 229 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 230 | #[serde(rename_all = "lowercase")] 231 | pub enum FilterType { 232 | NoOutput, 233 | Regex(RegexVariant), 234 | JmesPath(String), 235 | } 236 | 237 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 238 | #[serde(untagged)] 239 | pub enum RegexVariant { 240 | MatchOnly(String), 241 | Options(RegexOptions), 242 | } 243 | 244 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 245 | pub struct RegexOptions { 246 | matches: String, 247 | group: String, 248 | } 249 | 250 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 251 | #[serde(rename_all = "lowercase")] 252 | pub enum ExpectType { 253 | Anything, 254 | Matches(String), 255 | MatchesNot(String), 256 | GreaterThan(f64), 257 | LessThan(f64), 258 | } 259 | 260 | impl FilterType { 261 | fn filter(&self, val: &str) -> Result { 262 | match *self { 263 | FilterType::NoOutput => Ok(String::from("")), 264 | FilterType::JmesPath(ref jmes) => { 265 | let expr = jmespath::compile(jmes) 266 | .map_err(|err| format!("Could not compile jmespath:{}", err))?; 267 | 268 | let data = Variable::from_json(val) 269 | .map_err(|err| format!("Could not format as json:{}", err))?; 270 | 271 | let result = expr 272 | .search(data) 273 | .map_err(|err| format!("Could not find jmes expression:{}", err))?; 274 | 275 | let output = match &*result { 276 | Variable::String(val) => val.clone(), 277 | other => other.to_string(), 278 | }; 279 | 280 | if output != "null" { 281 | Ok(output) 282 | } else { 283 | Err(format!( 284 | "Could not find jmespath expression `{}` in output", 285 | expr 286 | )) 287 | } 288 | } 289 | FilterType::Regex(ref regex_var) => { 290 | let opts = match regex_var { 291 | RegexVariant::MatchOnly(ref string) => RegexOptions { 292 | matches: string.clone(), 293 | group: "0".into(), 294 | }, 295 | RegexVariant::Options(ref opts) => opts.clone(), 296 | }; 297 | 298 | let regex = Regex::new(&opts.matches).map_err(|err| { 299 | format!( 300 | "Could not create regex from `{}`. Error is:{:?}", 301 | &opts.matches, err 302 | ) 303 | })?; 304 | 305 | let captures = regex 306 | .captures(val) 307 | .ok_or_else(|| format!("Could not find `{}` in output", &opts.matches))?; 308 | 309 | match opts.group.parse::() { 310 | Ok(num) => { 311 | return captures 312 | .get(num) 313 | .map(|val| val.as_str().into()) 314 | .ok_or_else(|| { 315 | format!( 316 | "Could not find group number `{}` in regex `{}`", 317 | opts.group, opts.matches 318 | ) 319 | }); 320 | } 321 | Err(_) => { 322 | return captures 323 | .name(&opts.group) 324 | .map(|val| val.as_str().into()) 325 | .ok_or_else(|| { 326 | format!( 327 | "Could not find group name `{}` in regex `{}`", 328 | opts.group, opts.matches 329 | ) 330 | }); 331 | } 332 | } 333 | } 334 | } 335 | } 336 | } 337 | 338 | fn output_renderer(input: &str) -> Result { 339 | let cow_body = REGEX_OUTPUT.replace_all(input, "{{$1}}"); 340 | 341 | match cow_body { 342 | Cow::Borrowed(_) => Ok(input.to_string()), 343 | Cow::Owned(cow_body) => { 344 | let mut tera = Tera::default(); 345 | 346 | tera.add_raw_template("step_body", &cow_body) 347 | .map_err(|err| format!("Template Error: {}", err))?; 348 | 349 | let step_output: HashMap = STEP_OUTPUT.clone().into_iter().collect(); 350 | 351 | let mut context = HashMap::new(); 352 | context.insert("step_output", step_output); 353 | 354 | let body_rendered = tera 355 | .render( 356 | "step_body", 357 | &Context::from_serialize(&context) 358 | .map_err(|err| format!("Context Error: {}", err))?, 359 | ) 360 | .map_err(|err| format!("Template Rendering Error: {:?}", err))?; 361 | 362 | Ok(body_rendered) 363 | } 364 | } 365 | } 366 | 367 | lazy_static! { 368 | static ref NUMBER_FILTER: Regex = Regex::new("[^-0-9.,]").unwrap(); 369 | } 370 | 371 | impl ExpectType { 372 | fn check(&self, val: &str) -> Result<(), String> { 373 | match *self { 374 | ExpectType::Anything => Ok(()), 375 | ExpectType::MatchesNot(ref match_string) => { 376 | let regex = Regex::new(match_string).map_err(|err| { 377 | format!( 378 | "Could not create regex from `{}`. Error is:{:?}", 379 | match_string, err 380 | ) 381 | })?; 382 | 383 | if !regex.is_match(val) { 384 | Ok(()) 385 | } else { 386 | Err(format!("Matched against `{}`", match_string)) 387 | } 388 | } 389 | ExpectType::Matches(ref match_string) => { 390 | let regex = Regex::new(match_string).map_err(|err| { 391 | format!( 392 | "Could not create regex from `{}`. Error is:{:?}", 393 | match_string, err 394 | ) 395 | })?; 396 | 397 | if regex.is_match(val) { 398 | Ok(()) 399 | } else { 400 | Err(format!("Not matched against `{}`", match_string)) 401 | } 402 | } 403 | ExpectType::GreaterThan(ref num) => { 404 | match NUMBER_FILTER.replace_all(val, "").parse::() { 405 | Ok(compare) => { 406 | if compare > *num { 407 | Ok(()) 408 | } else { 409 | Err(format!( 410 | "The value `{}` is not greater than `{}`", 411 | compare, num 412 | )) 413 | } 414 | } 415 | Err(_) => Err(format!("Could not parse `{}` as a number", val)), 416 | } 417 | } 418 | ExpectType::LessThan(ref num) => { 419 | match NUMBER_FILTER.replace_all(val, "").parse::() { 420 | Ok(compare) => { 421 | if compare < *num { 422 | Ok(()) 423 | } else { 424 | Err(format!( 425 | "The value `{}` is not less than `{}`", 426 | compare, num 427 | )) 428 | } 429 | } 430 | Err(_) => Err(format!("Could not parse `{}` as a number", num)), 431 | } 432 | } 433 | } 434 | } 435 | } 436 | 437 | impl Default for ExpectType { 438 | fn default() -> Self { 439 | ExpectType::Anything 440 | } 441 | } 442 | 443 | #[cfg(test)] 444 | mod tests { 445 | use super::*; 446 | 447 | #[test] 448 | fn expect_negative_numbers() { 449 | let expect = ExpectType::LessThan(0.0); 450 | assert_eq!(expect.check("-1"), Ok(())); 451 | assert_eq!(expect.check("-1.0"), Ok(())); 452 | assert_eq!(expect.check("-.01"), Ok(())); 453 | assert_eq!(expect.check("-0.01"), Ok(())); 454 | 455 | let expect = ExpectType::GreaterThan(-2.0); 456 | assert_eq!(expect.check("-1"), Ok(())); 457 | assert_eq!(expect.check("-1.0"), Ok(())); 458 | assert_eq!(expect.check("-.01"), Ok(())); 459 | assert_eq!(expect.check("-0.01"), Ok(())); 460 | } 461 | } 462 | -------------------------------------------------------------------------------- /src/step/system.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use lazy_static::lazy_static; 4 | use sys_info::{disk_info, loadavg, mem_info}; 5 | use tokio::sync::Mutex; 6 | 7 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 8 | #[serde(rename_all = "snake_case")] 9 | pub enum SystemVariant { 10 | MemTotal, 11 | MemFree, 12 | MemAvailable, 13 | LoadAvg1m, 14 | LoadAvg5m, 15 | LoadAvg15m, 16 | DiskTotal, 17 | DiskFree, 18 | } 19 | 20 | lazy_static! { 21 | static ref SYS_MUTEX: Mutex<()> = Mutex::new(()); 22 | } 23 | 24 | impl SystemVariant { 25 | pub async fn run(&self) -> Result { 26 | // This is a workaround for a memory bug in `sys_info` 27 | // See: https://github.com/FillZpp/sys-info-rs/issues/63 28 | let _guard = SYS_MUTEX.lock().await; 29 | match self { 30 | SystemVariant::LoadAvg1m => loadavg() 31 | .map(|load| load.one.to_string()) 32 | .map_err(|_| "Could not get load".to_string()), 33 | SystemVariant::LoadAvg5m => loadavg() 34 | .map(|load| load.five.to_string()) 35 | .map_err(|_| "Could not get load".to_string()), 36 | SystemVariant::LoadAvg15m => loadavg() 37 | .map(|load| load.fifteen.to_string()) 38 | .map_err(|_| "Could not get load".to_string()), 39 | SystemVariant::MemAvailable => mem_info() 40 | .map(|mem| mem.avail.to_string()) 41 | .map_err(|_| "Could not get memory".to_string()), 42 | SystemVariant::MemFree => mem_info() 43 | .map(|mem| mem.free.to_string()) 44 | .map_err(|_| "Could not get memory".to_string()), 45 | SystemVariant::MemTotal => mem_info() 46 | .map(|mem| mem.total.to_string()) 47 | .map_err(|_| "Could not get memory".to_string()), 48 | SystemVariant::DiskTotal => disk_info() 49 | .map(|disk| disk.total.to_string()) 50 | .map_err(|_| "Could not get disk".to_string()), 51 | SystemVariant::DiskFree => disk_info() 52 | .map(|disk| disk.free.to_string()) 53 | .map_err(|_| "Could not get disk".to_string()), 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/submitter.rs: -------------------------------------------------------------------------------- 1 | use colored::*; 2 | use reqwest::IntoUrl; 3 | use serde::{Deserialize, Serialize}; 4 | use serde_json::json; 5 | 6 | use std::convert::From; 7 | 8 | use crate::step::Step; 9 | 10 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 11 | pub struct StepResult { 12 | pub name: String, 13 | pub description: Option, 14 | pub pass: bool, 15 | pub output: String, 16 | pub error: Option, 17 | pub on_fail_output: Option, 18 | pub on_fail_error: Option, 19 | pub duration: f32, 20 | } 21 | 22 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 23 | pub struct WebHook { 24 | hostname: String, 25 | has_errors: bool, 26 | tests: Vec, 27 | } 28 | 29 | pub async fn submit_slack>( 30 | results: &[StepResult], 31 | url: U, 32 | hostname: I, 33 | ) -> Result<(), reqwest::Error> { 34 | let num_errors = results.iter().filter(|result| !result.pass).count(); 35 | 36 | if num_errors == 0 { 37 | return Ok(()); 38 | } 39 | 40 | let mut blocks = vec![]; 41 | 42 | let title = format!( 43 | "{} Error{} from `{}`", 44 | num_errors, 45 | if num_errors == 1 { "" } else { "s" }, 46 | hostname.into() 47 | ); 48 | 49 | blocks.push(json!({ 50 | "type": "header", 51 | "text": { 52 | "type": "plain_text", 53 | "text": &title, 54 | "emoji": true 55 | } 56 | })); 57 | 58 | for result in results.iter().filter(|result| !result.pass) { 59 | let mut text = format!("*Name*: {}", result.name); 60 | 61 | if let Some(ref val) = result.description { 62 | text.push_str(&format!(", *Description*: {}\n\n", val)); 63 | } else { 64 | text.push_str("\n\n") 65 | } 66 | 67 | if let Some(ref val) = result.error { 68 | text.push_str(&format!("*Error*: {}\n\n", val)); 69 | } 70 | 71 | if result.output.is_empty() { 72 | text.push_str(&format!("*Duration*: ({:.2}ms)\n\n", result.duration)); 73 | } else { 74 | text.push_str(&format!("*Output*: ({:.2}ms)\n\n", result.duration)); 75 | } 76 | 77 | blocks.push(json!({ 78 | "type": "section", 79 | "text": { 80 | "type": "mrkdwn", 81 | "text": truncate(&text, 3000) 82 | } 83 | })); 84 | 85 | if !result.output.is_empty() { 86 | blocks.push(json!({ 87 | "type": "rich_text", 88 | "elements": [ 89 | { 90 | "type": "rich_text_preformatted", 91 | "elements": [ 92 | { 93 | "type": "text", 94 | "text": truncate(&result.output, 3000) 95 | } 96 | ] 97 | } 98 | ] 99 | 100 | })); 101 | } 102 | } 103 | 104 | let payload = json!( 105 | { 106 | "text": &title, 107 | "blocks": blocks 108 | } 109 | ); 110 | 111 | let client = reqwest::Client::new(); 112 | 113 | let builder = client.post(url); 114 | 115 | let builder = builder.json(&payload); 116 | 117 | let response = builder.send().await?; 118 | 119 | if !response.status().is_success() { 120 | eprintln!("Error submitting slack webhook:"); 121 | eprintln!("Status: {}", response.status()); 122 | let val = response.text().await?; 123 | eprintln!("Body: {}", val); 124 | } 125 | 126 | Ok(()) 127 | } 128 | 129 | pub async fn submit_webhook>( 130 | results: &[StepResult], 131 | url: U, 132 | hostname: I, 133 | ) -> Result<(), reqwest::Error> { 134 | let has_errors = results.iter().any(|result| !result.pass); 135 | 136 | let payload = WebHook { 137 | hostname: hostname.into(), 138 | has_errors, 139 | tests: results.to_vec(), 140 | }; 141 | 142 | let client = reqwest::Client::new(); 143 | 144 | let builder = client.post(url); 145 | 146 | let builder = builder.json(&payload); 147 | 148 | let response = builder.send().await?; 149 | 150 | if !response.status().is_success() { 151 | eprintln!("Error submitting webhook:"); 152 | eprintln!("Status: {}", response.status()); 153 | let val = response.text().await?; 154 | eprintln!("Body: {}", val); 155 | } 156 | 157 | Ok(()) 158 | } 159 | 160 | impl StepResult { 161 | pub fn terminal_print(&self, colours: &bool) { 162 | let mut message = format!("- name: {}\n", self.name); 163 | 164 | if let Some(ref description) = self.description { 165 | message.push_str(&format!(" description: {}\n", description)) 166 | } 167 | 168 | message.push_str(&format!(" pass: {}\n", self.pass)); 169 | 170 | if !self.output.is_empty() { 171 | if self.output.contains('\n') { 172 | message.push_str(&format!( 173 | " output: |\n {}\n", 174 | self.output.replace("\n", "\n ") 175 | )); 176 | } else { 177 | message.push_str(&format!(" output: {}\n", self.output)); 178 | } 179 | } 180 | if let Some(ref error) = self.error { 181 | message.push_str(&format!(" error: {}\n", error)); 182 | } 183 | 184 | if let Some(ref output) = self.on_fail_output { 185 | if !output.trim().is_empty() { 186 | message.push_str(&format!(" on_fail_output: {}\n", output)); 187 | } 188 | } 189 | 190 | if let Some(ref error) = self.on_fail_error { 191 | message.push_str(&format!(" on_fail_error: {}\n", error)); 192 | } 193 | 194 | message.push_str(&format!(" duration: {}ms\n", self.duration)); 195 | 196 | if *colours { 197 | match self.pass { 198 | true => { 199 | println!("{}", message.green().bold()); 200 | } 201 | false => { 202 | println!("{}", message.red().bold()); 203 | } 204 | } 205 | } else { 206 | println!("{}", message); 207 | } 208 | } 209 | } 210 | 211 | impl From for StepResult { 212 | fn from(step: Step) -> Self { 213 | let duration = step.get_duration_ms(); 214 | let name = step.name; 215 | let description = step.description; 216 | 217 | let (pass, output, error, on_fail_output, on_fail_error) = match step.outcome { 218 | Some(outcome) => { 219 | let output = match step.do_output { 220 | true => outcome.output.unwrap_or_default(), 221 | false => String::new(), 222 | }; 223 | 224 | ( 225 | outcome.error.is_none(), 226 | output, 227 | outcome.error, 228 | outcome.on_fail_output, 229 | outcome.on_fail_error, 230 | ) 231 | } 232 | None => ( 233 | false, 234 | String::new(), 235 | Some(String::from("Not finished")), 236 | None, 237 | None, 238 | ), 239 | }; 240 | 241 | StepResult { 242 | name, 243 | duration, 244 | description, 245 | pass, 246 | output, 247 | on_fail_output, 248 | on_fail_error, 249 | error, 250 | } 251 | } 252 | } 253 | 254 | pub fn truncate(input: &str, len: usize) -> String { 255 | if input.len() <= len { 256 | return input.to_string(); 257 | } 258 | 259 | let mut end_idx = len + 1; 260 | 261 | while !input.is_char_boundary(end_idx) { 262 | end_idx -= 1; 263 | } 264 | 265 | let slice = &input[0..end_idx]; 266 | 267 | let mut end_idx = len; 268 | 269 | if let Some(val) = slice.rfind(char::is_whitespace) { 270 | end_idx = val; 271 | } 272 | 273 | return format!("{}...", &input[0..end_idx]); 274 | } 275 | -------------------------------------------------------------------------------- /src/yaml.rs: -------------------------------------------------------------------------------- 1 | use crate::step::FilterType; 2 | use crate::step::RegexVariant; 3 | use std::fs::File; 4 | 5 | use log::debug; 6 | use serde::{Deserialize, Serialize}; 7 | 8 | use serde_yaml::{self, Value}; 9 | use tera::{Context, Tera}; 10 | 11 | use std::path::Path; 12 | 13 | use anyhow::{anyhow, Error}; 14 | use std::io::Read; 15 | 16 | use crate::step::{ 17 | BashVariant, DiskVariant, ExpectType, HttpVariant, Requirement, RetryPolicy, RunType, Step, 18 | SystemVariant, 19 | }; 20 | use linked_hash_map::LinkedHashMap; 21 | 22 | #[derive(Debug, PartialEq, Deserialize)] 23 | struct StepYaml { 24 | description: Option, 25 | value: Option, 26 | bash: Option, 27 | step: Option, 28 | http: Option, 29 | system: Option, 30 | disk: Option, 31 | matches: Option, 32 | matches_not: Option, 33 | #[serde(default)] 34 | filters: Vec, 35 | jmespath: Option, 36 | regex: Option, 37 | do_output: Option, 38 | less_than: Option, 39 | greater_than: Option, 40 | retry_count: Option, 41 | retry_delay_ms: Option, 42 | delay_ms: Option, 43 | on_fail: Option, 44 | require: Option, 45 | required_by: Option, 46 | } 47 | 48 | fn get_retry_policy(step: &StepYaml) -> RetryPolicy { 49 | let retry_delay_ms = step.retry_delay_ms.unwrap_or_default(); 50 | let retry_count = step.retry_count.unwrap_or_default(); 51 | let initial_delay_ms = step.delay_ms.unwrap_or_default(); 52 | 53 | RetryPolicy { 54 | retry_count, 55 | retry_delay_ms, 56 | initial_delay_ms, 57 | } 58 | } 59 | 60 | fn get_runtype(step: &StepYaml) -> RunType { 61 | if let Some(ref step) = step.step { 62 | return RunType::Step(step.clone()); 63 | } 64 | 65 | if let Some(ref variant) = step.bash { 66 | return RunType::Bash(variant.clone()); 67 | } 68 | 69 | if let Some(ref variant) = step.http { 70 | return RunType::Http(variant.clone()); 71 | } 72 | 73 | if let Some(ref variant) = step.system { 74 | return RunType::System(variant.clone()); 75 | } 76 | 77 | if let Some(ref variant) = step.disk { 78 | return RunType::Disk(variant.clone()); 79 | } 80 | 81 | RunType::Value(step.value.clone().unwrap_or_default()) 82 | } 83 | 84 | fn get_expecttype(step: &StepYaml) -> ExpectType { 85 | if let Some(ref string) = step.matches { 86 | return ExpectType::Matches(string.clone()); 87 | } 88 | 89 | if let Some(ref string) = step.matches_not { 90 | return ExpectType::MatchesNot(string.clone()); 91 | } 92 | 93 | if let Some(ref string) = step.greater_than { 94 | return ExpectType::GreaterThan(string.parse().expect("Could not parse number")); 95 | } 96 | 97 | if let Some(ref string) = step.less_than { 98 | return ExpectType::LessThan(string.parse().expect("Could not parse number")); 99 | } 100 | 101 | ExpectType::Anything 102 | } 103 | 104 | fn get_filters(step: &StepYaml) -> Vec { 105 | let mut filters: Vec = step.filters.clone(); 106 | 107 | if let Some(ref jmespath) = step.jmespath { 108 | filters.push(FilterType::JmesPath(jmespath.clone())) 109 | }; 110 | 111 | if let Some(ref variant) = step.regex { 112 | filters.push(FilterType::Regex(variant.clone())) 113 | }; 114 | 115 | filters 116 | } 117 | 118 | pub fn get_steps_raw(yaml_contents: &str, context: &T) -> Result, Error> { 119 | let mut tera = Tera::default(); 120 | 121 | tera.add_raw_template("test_plan", yaml_contents)?; 122 | 123 | let test_plan_yaml = tera.render("test_plan", &Context::from_serialize(context)?)?; 124 | 125 | debug!("YAML output:\n{}", test_plan_yaml); 126 | 127 | let input_steps: LinkedHashMap = serde_yaml::from_str(&test_plan_yaml)?; 128 | let mut steps: Vec = Vec::new(); 129 | 130 | for (name, step) in input_steps { 131 | let run = get_runtype(&step); 132 | 133 | let expect = get_expecttype(&step); 134 | 135 | let filters = get_filters(&step); 136 | 137 | let retry_policy = get_retry_policy(&step); 138 | 139 | steps.push(Step { 140 | name, 141 | run, 142 | on_fail: step.on_fail, 143 | do_output: step.do_output.unwrap_or(true), 144 | expect, 145 | description: step.description, 146 | filters, 147 | retry: retry_policy, 148 | outcome: None, 149 | require: step 150 | .require 151 | .map(|require| require.to_vec()) 152 | .unwrap_or_default(), 153 | required_by: step 154 | .required_by 155 | .map(|require| require.to_vec()) 156 | .unwrap_or_default(), 157 | }); 158 | } 159 | 160 | Ok(steps) 161 | } 162 | 163 | //We use P & Q here so that when specialising file path and config path can be different types, i.e, a &str & Option for instance.. 164 | pub fn get_steps, Q: AsRef>( 165 | file_path: P, 166 | config_path: &Option, 167 | ) -> Result, Error> { 168 | let mut file_contents = String::new(); 169 | 170 | let path_ref = file_path.as_ref(); 171 | 172 | let mut f = File::open(path_ref) 173 | .map_err(|err| anyhow!("Could not open file {:?}: {}", path_ref, err))?; 174 | 175 | f.read_to_string(&mut file_contents)?; 176 | 177 | match *config_path { 178 | Some(ref path) => { 179 | let c = File::open(path)?; 180 | 181 | let value: Value = serde_yaml::from_reader(c).map_err(|err| { 182 | anyhow!( 183 | "Could not parse config {:?} as YAML: {}", 184 | path.as_ref(), 185 | err 186 | ) 187 | })?; 188 | 189 | get_steps_raw(&file_contents, &value) 190 | .map_err(|err| anyhow!("Could not parse file {:?}: {}", path_ref, err)) 191 | } 192 | None => get_steps_raw(&file_contents, &Value::Mapping(serde_yaml::Mapping::new())) 193 | .map_err(|err| anyhow!("Could not parse file {:?}: {}", path_ref, err)), 194 | } 195 | } 196 | --------------------------------------------------------------------------------