diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c3b73c1d2..c3ffbe16b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,10 +24,38 @@ jobs: - run: npm run build - run: npm run lint + + build-rust: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Install Rust Stable toolchain + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + + - name: Run cargo fmt + run: cargo fmt --all -- --check + + - uses: actions-rs/clippy-check@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + args: --all-features + + - name: Build + run: cargo build --verbose + + - name: Run tests + run: cargo test --verbose --all-targets --all-features + + all: # This dummy job depends on all the mandatory checks. It succeeds if and only if all CI checks # are successful. - needs: [build] + needs: [build, build-rust] runs-on: ubuntu-latest steps: - run: echo Success diff --git a/.gitignore b/.gitignore index c2a49246b..300d12c7c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,7 @@ node_modules dist log.md .env -bins \ No newline at end of file +bins +**/target/ +*.swp +.vscode diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 000000000..5ce681ebb --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1504 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + +[[package]] +name = "async-compression" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "345fd392ab01f746c717b1357165b76f0b67a60192007b234058c9045fdcf695" +dependencies = [ + "brotli", + "flate2", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "async-trait" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76464446b8bc32758d7e88ee1a804d9914cd9b1cb264c029899680b0be29826f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "axum" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b4e96976b2022b23b2199168ff9b281e9ddc1aa795607d5cb7146868ca5c101" +dependencies = [ + "async-trait", + "base64", + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "hyper", + "matchit", + "mime", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "sha-1 0.9.8", + "sync_wrapper", + "tokio", + "tokio-tungstenite", + "tokio-util 0.6.10", + "tower", + "tower-http", + "tower-layer", + "tower-service", +] + +[[package]] +name = "backchannel" +version = "0.1.0" +dependencies = [ + "axum", + "futures", + "serde", + "serde_json", + "tokio", + "tokio-stream", + "tower", + "tower-http", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" +dependencies = [ + "generic-array", +] + +[[package]] +name = "brotli" +version = "3.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ad2d4653bf5ca36ae797b1f4bb4dbddb60ce49ca4aed8a2ce4829f60425b80" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bumpalo" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "3.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b71c3ce99b7611011217b366d923f1d0a7e07a92bb2dbf1e84508c673ca3bd" +dependencies = [ + "atty", + "bitflags", + "clap_derive", + "clap_lex", + "indexmap", + "once_cell", + "strsim", + "termcolor", + "textwrap", +] + +[[package]] +name = "clap_derive" +version = "3.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea0c8bce528c4be4da13ea6fead8965e95b6073585a2f05204bd8f4119f82a65" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + +[[package]] +name = "cpufeatures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "digest" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" +dependencies = [ + "block-buffer 0.10.3", + "crypto-common", +] + +[[package]] +name = "flate2" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs-err" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64db3e262960f0662f43a6366788d5f10f7f244b8f7d7d987f560baf5ded5c50" + +[[package]] +name = "futures" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f21eda599937fba36daeb58a22e8f5cee2d14c4a17b5b7739c7c8e5e3b8230c" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30bdd20c28fadd505d0fd6712cdfcb0d4b5648baf45faef7f852afb2399bb050" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e5aa3de05362c3fb88de6531e6296e85cde7739cccad4b9dfeeb7f6ebce56bf" + +[[package]] +name = "futures-executor" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ff63c23854bee61b6e9cd331d523909f238fc7636290b96826e9cfa5faa00ab" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbf4d2a7a308fd4578637c0b17c7e1c7ba127b8f6ba00b29f717e9655d85eb68" + +[[package]] +name = "futures-macro" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42cd15d1c7456c04dbdf7e88bcd69760d74f3a798d6444e16974b505b0e62f17" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b20ba5a92e727ba30e72834706623d94ac93a725410b6a6b6fbc1b07f7ba56" + +[[package]] +name = "futures-task" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6508c467c73851293f390476d4491cf4d227dbabcd4170f3bb6044959b294f1" + +[[package]] +name = "futures-util" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44fb6cb1be61cc1d2e43b262516aafcf63b241cffdb1d3fa115f91d9c7b09c90" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "http" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" + +[[package]] +name = "hyper" +version = "0.14.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02c929dc5c39e335a03c405292728118860721b10190d98c2a0f0efd5baafbac" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "idna" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.132" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" + +[[package]] +name = "lock_api" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matchit" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9376a4f0340565ad675d11fc1419227faf5f60cd7ac9cb2e7185a471f30af833" + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "mime" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" + +[[package]] +name = "miniz_oxide" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96590ba8f175222643a85693f33d26e9c8a015f599c216509b1a6894af675d34" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys", +] + +[[package]] +name = "num_cpus" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "os_str_bytes" +version = "6.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-sys", +] + +[[package]] +name = "parser" +version = "0.0.1" +dependencies = [ + "clap", + "fs-err", + "pest", + "pest_derive", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "percent-encoding" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" + +[[package]] +name = "pest" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b0560d531d1febc25a3c9398a62a71256c0178f2e3443baedd9ad4bb8c9deb4" +dependencies = [ + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "905708f7f674518498c1f8d644481440f476d39ca6ecae83319bba7c6c12da91" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5803d8284a629cc999094ecd630f55e91b561a1d1ba75e233b00ae13b91a69ad" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1538eb784f07615c6d9a8ab061089c6c54a344c5b4301db51990ca1c241e8c04" +dependencies = [ + "once_cell", + "pest", + "sha-1 0.10.0", +] + +[[package]] +name = "pin-project" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "ppv-lite86" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" +dependencies = [ + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" + +[[package]] +name = "ryu" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "serde" +version = "1.0.144" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.144" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha-1" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "sha-1" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.3", +] + +[[package]] +name = "sharded-slab" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" + +[[package]] +name = "socket2" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "1.0.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20518fe4a4c9acf048008599e464deb21beeae3d3578418951a189c235a7a9a8" + +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" + +[[package]] +name = "thiserror" +version = "1.0.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c1b05ca9d106ba7d2e31a9dab4a64e7be2cce415321966ea3132c49a656e252" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8f2591983642de85c921015f3f070c665a197ed69e417af436115e3a1407487" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "tokio" +version = "1.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0020c875007ad96677dcc890298f4b942882c5d4eb7cc8f439fc3bf813dc9c95" +dependencies = [ + "autocfg", + "bytes", + "libc", + "memchr", + "mio", + "num_cpus", + "once_cell", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "winapi", +] + +[[package]] +name = "tokio-macros" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-stream" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6edf2d6bc038a43d31353570e27270603f4648d18f5ed10c0e179abe43255af" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "511de3f85caf1c98983545490c3d09685fa8eb634e57eec22bb4db271f46cbd8" +dependencies = [ + "futures-util", + "log", + "pin-project", + "tokio", + "tungstenite", +] + +[[package]] +name = "tokio-util" +version = "0.6.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36943ee01a6d67977dd3f84a5a1d2efeb4ada3a1ae771cadfaa535d9d9fc6507" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "log", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb2e075f03b3d66d8d8785356224ba688d2906a371015e225beeb65ca92c740" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tokio-util 0.7.4", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81eca72647e58054bbfa41e6f297c23436f1c60aff6e5eb38455a0f9ca420bb5" +dependencies = [ + "async-compression", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "pin-project", + "tokio", + "tokio-util 0.6.10", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "343bc9466d3fe6b0f960ef45960509f84480bf4fd96f92901afe7ff3df9d3a62" + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fce9567bd60a67d08a16488756721ba392f24f29006402881e43b19aac64307" +dependencies = [ + "cfg-if", + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11c75893af559bc8e10716548bdef5cb2b983f8e637db9d0e15126b61b484ee2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeea4303076558a00714b823f9ad67d58a3bbda1df83d8827d21193156e22f7" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" +dependencies = [ + "lazy_static", + "log", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60db860322da191b40952ad9affe65ea23e7dd6a5c442c2c42865810c6ab8e6b" +dependencies = [ + "ansi_term", + "matchers", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" + +[[package]] +name = "tungstenite" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0b2d8558abd2e276b0a8df5c05a2ec762609344191e5fd23e292c910e9165b5" +dependencies = [ + "base64", + "byteorder", + "bytes", + "http", + "httparse", + "log", + "rand", + "sha-1 0.9.8", + "thiserror", + "url", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" + +[[package]] +name = "ucd-trie" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81" + +[[package]] +name = "unicode-bidi" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" + +[[package]] +name = "unicode-ident" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "url" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "want" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +dependencies = [ + "log", + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" +dependencies = [ + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" + +[[package]] +name = "windows_i686_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" + +[[package]] +name = "windows_i686_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" + +[[package]] +name = "zombienet-dsl-parser-wrapper" +version = "0.1.3" +dependencies = [ + "parser", + "serde_json", + "wasm-bindgen", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 000000000..d80d20241 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,4 @@ +[workspace] + +members = [ "crates/*", "backchannel" ] +exclude = ["backchannel-cli"] diff --git a/backchannel/src/main.rs b/backchannel/src/main.rs index 95df442e7..73e5123ae 100644 --- a/backchannel/src/main.rs +++ b/backchannel/src/main.rs @@ -98,7 +98,7 @@ async fn kv_set( ) { let item: DbItem = DbItem { key: key.clone(), - value: String::from_utf8(bytes.to_vec()).unwrap() + value: String::from_utf8(bytes.to_vec()).unwrap(), }; state.db.write().unwrap().insert(key, bytes); let _ = state.tx.send(serde_json::to_string(&item).unwrap()); diff --git a/crates/parser-wrapper/Cargo.toml b/crates/parser-wrapper/Cargo.toml new file mode 100644 index 000000000..7575904a2 --- /dev/null +++ b/crates/parser-wrapper/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "zombienet-dsl-parser-wrapper" +version = "0.1.3" +edition = "2021" +description = "Zombienet DSL parser: produces a test definition, in json format, that can be used with the ZombieNet's test-runnner." +license = "GPL-3.0-or-later" +authors = ["Parity Technologies ", "Javier Viola "] +repository = "https://github.com/paritytech/zombienet" + +[lib] +name = "parser_wrapper" +path = "src/lib.rs" +crate-type = ["cdylib", "rlib"] + +[dependencies] +parser = { path = "../parser" } +wasm-bindgen = "0.2.63" +serde_json = "1.0.85" diff --git a/crates/parser-wrapper/src/lib.rs b/crates/parser-wrapper/src/lib.rs new file mode 100644 index 000000000..137860d13 --- /dev/null +++ b/crates/parser-wrapper/src/lib.rs @@ -0,0 +1,13 @@ +use parser::parse; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +pub fn parse_to_json(unparsed_file: &str) -> Result { + if unparsed_file.is_empty() { + return Err("Provided test specification is empty".to_string()); + } + let ast = parse(unparsed_file).map_err(|e| e.to_string())?; + let ast_json = + serde_json::to_string_pretty(&ast).map_err(|_| "Serializing error".to_string())?; + Ok(ast_json) +} diff --git a/crates/parser/Cargo.lock b/crates/parser/Cargo.lock new file mode 100644 index 000000000..cb18012d1 --- /dev/null +++ b/crates/parser/Cargo.lock @@ -0,0 +1,251 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "block-buffer" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" +dependencies = [ + "generic-array", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cpufeatures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "generic-array" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "itoa" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754" + +[[package]] +name = "libc" +version = "0.2.132" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" + +[[package]] +name = "once_cell" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0" + +[[package]] +name = "pest" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b0560d531d1febc25a3c9398a62a71256c0178f2e3443baedd9ad4bb8c9deb4" +dependencies = [ + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "905708f7f674518498c1f8d644481440f476d39ca6ecae83319bba7c6c12da91" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5803d8284a629cc999094ecd630f55e91b561a1d1ba75e233b00ae13b91a69ad" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1538eb784f07615c6d9a8ab061089c6c54a344c5b4301db51990ca1c241e8c04" +dependencies = [ + "once_cell", + "pest", + "sha-1", +] + +[[package]] +name = "proc-macro2" +version = "1.0.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ryu" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" + +[[package]] +name = "serde" +version = "1.0.144" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.144" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha-1" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "syn" +version = "1.0.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c1b05ca9d106ba7d2e31a9dab4a64e7be2cce415321966ea3132c49a656e252" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8f2591983642de85c921015f3f070c665a197ed69e417af436115e3a1407487" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "typenum" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" + +[[package]] +name = "ucd-trie" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81" + +[[package]] +name = "unicode-ident" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "zombienet-parser" +version = "0.1.0" +dependencies = [ + "pest", + "pest_derive", + "serde", + "serde_json", + "thiserror", +] diff --git a/crates/parser/Cargo.toml b/crates/parser/Cargo.toml new file mode 100644 index 000000000..e0789468f --- /dev/null +++ b/crates/parser/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "parser" +version = "0.0.1" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[[bin]] +name = "zombienet-parser-cli" +path = "src/cli.rs" + +[lib] +name = "parser" +path = "src/lib.rs" + +[dependencies] +clap = { version = "3.2.20", features = ["derive"] } +pest = "2.3.0" +pest_derive = "2.3.0" +serde = { version = "1.0.144", features = ["derive"] } +serde_json = "1.0.85" +thiserror = "1.0.34" +fs-err = "2.8.1" diff --git a/crates/parser/src/ast.rs b/crates/parser/src/ast.rs new file mode 100644 index 000000000..cae521103 --- /dev/null +++ b/crates/parser/src/ast.rs @@ -0,0 +1,175 @@ +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; +use std::time::Duration; + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Copy, Clone)] +pub enum Operator { + Equal, + NotEqual, + IsAbove, + IsAtLeast, + IsBelow, + IsAtMost, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +pub struct Comparison { + pub op: Operator, + pub target_value: u64, +} + +pub type ParaId = u16; +pub type NodeName = String; + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(tag = "fn", content = "args")] +pub enum AssertionKind { + IsUp { + node_name: NodeName, + #[serde(with = "optional_timeout")] + timeout: Option, + }, + ParaIsRegistered { + node_name: NodeName, + para_id: ParaId, + #[serde(with = "optional_timeout")] + timeout: Option, + }, + ParaBlockHeight { + node_name: NodeName, + para_id: ParaId, + op: Operator, + target_value: u64, + #[serde(with = "optional_timeout")] + timeout: Option, + }, + ParaRuntimeUpgrade { + node_name: NodeName, + para_id: ParaId, + file_or_uri: String, + #[serde(with = "optional_timeout")] + timeout: Option, + }, + ParaRuntimeDummyUpgrade { + node_name: NodeName, + para_id: ParaId, + #[serde(with = "optional_timeout")] + timeout: Option, + }, + Histogram { + node_name: NodeName, + metric_name: String, + op: Operator, + target_value: u64, + buckets: Vec, + #[serde(with = "optional_timeout")] + timeout: Option, + }, + Report { + node_name: NodeName, + metric_name: String, + op: Operator, + target_value: u64, + #[serde(with = "optional_timeout")] + timeout: Option, + }, + LogMatch { + node_name: NodeName, + match_type: String, + pattern: String, + #[serde(with = "optional_timeout")] + timeout: Option, + }, + Trace { + node_name: NodeName, + span_id: String, + pattern: String, + #[serde(with = "optional_timeout")] + timeout: Option, + }, + SystemEvent { + node_name: NodeName, + match_type: String, + pattern: String, + #[serde(with = "optional_timeout")] + timeout: Option, + }, + CustomJs { + node_name: NodeName, + file_path: PathBuf, + custom_args: Option, + #[serde(flatten)] + cmp: Option, + #[serde(with = "optional_timeout")] + timeout: Option, + }, + CustomSh { + node_name: NodeName, + file_path: PathBuf, + custom_args: Option, + #[serde(flatten)] + cmp: Option, + #[serde(with = "optional_timeout")] + timeout: Option, + }, + Pause { + node_name: NodeName, + }, + Resume { + node_name: NodeName, + }, + Restart { + node_name: NodeName, + #[serde(with = "optional_timeout")] + after: Option, + }, + Sleep { + #[serde(with = "optional_timeout")] + seconds: Option, + }, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +pub struct Assertion { + pub original_line: String, + pub parsed: AssertionKind, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +pub struct TestDefinition { + pub description: Option, + pub network: String, + pub creds: String, + pub assertions: Vec, +} + +pub mod optional_timeout { + + use std::time::Duration; + + use serde::{self, Deserialize, Deserializer, Serializer}; + + pub fn serialize(timeout: &Option, serializer: S) -> Result + where + S: Serializer, + { + match timeout { + Some(secs) => serializer.serialize_some(&secs.as_secs()), + None => serializer.serialize_none(), + } + } + + /// Attempts to deserialize an u64 as Option + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + match u64::deserialize(deserializer) { + Ok(s) => Ok(Some(Duration::from_secs(s))), + Err(_) => { + // If we can deserialize to an u64 deserialize to None + Ok(None) + } + } + } +} diff --git a/crates/parser/src/cli.rs b/crates/parser/src/cli.rs new file mode 100644 index 000000000..125ac8cb3 --- /dev/null +++ b/crates/parser/src/cli.rs @@ -0,0 +1,28 @@ +use fs_err as fs; + +use clap::Parser; +use std::path::PathBuf; + +#[derive(Parser)] +#[clap(author, version, about, long_about = None)] +struct Cli { + /// Optional name to operate on + #[clap(value_parser)] + file_path: PathBuf, +} + +pub fn main() { + let cli = Cli::parse(); + let unparsed_file = fs::read_to_string(&cli.file_path) + .unwrap_or_else(|_| panic!("cannot read file {}", cli.file_path.to_string_lossy())); + + let a = parser::parse(&unparsed_file); + match a { + Ok(test_def) => { + println!("{}", serde_json::to_string_pretty(&test_def).unwrap()); + } + Err(e) => { + println!("{}", e); + } + } +} diff --git a/crates/parser/src/errors.rs b/crates/parser/src/errors.rs new file mode 100644 index 000000000..941b813af --- /dev/null +++ b/crates/parser/src/errors.rs @@ -0,0 +1,16 @@ +#[derive(Debug, thiserror::Error)] +#[allow(missing_docs)] +pub enum ParserError { + #[error("Error parsing file: {0}")] + ParseError(String), + #[error("Invalid matching rule. \n {0}")] + InvalidRule(String), + #[error("Missing fields: {0}")] + MissingFields(String), + #[error("Serialization error")] + SerializationError, + #[error("Unexpected rule: \n {0}")] + Unexpected(String), + #[error("Unreachable rule: \n {0}")] + UnreachableRule(String), +} diff --git a/crates/parser/src/lib.rs b/crates/parser/src/lib.rs new file mode 100644 index 000000000..5f586442a --- /dev/null +++ b/crates/parser/src/lib.rs @@ -0,0 +1,573 @@ +use std::path::PathBuf; +use std::time::Duration; + +use pest::{ + iterators::{Pair, Pairs}, + Parser, +}; +use pest_derive::Parser; + +pub mod ast; +mod errors; +use errors::ParserError; + +use ast::{Assertion, AssertionKind, Comparison, NodeName, ParaId, TestDefinition}; + +#[cfg(test)] +mod tests; + +// This include forces recompiling this source file if the grammar file changes. +// Uncomment it when doing changes to the .pest file +const _GRAMMAR: &str = include_str!("zombienet.pest"); + +#[derive(Parser)] +#[grammar = "zombienet.pest"] +pub struct ZombieNetParser; + +fn parse_name(pair: Pair) -> Result { + // get the first inner pair, since we don't want the `:` + match pair.into_inner().next() { + Some(p) => Ok(p.as_str().to_string()), + None => Err(ParserError::Unexpected(String::from( + "Rule should have an inner rule", + ))), + } +} + +fn parse_within(pair: Pair) -> Result { + let within = pair.into_inner().as_str(); + Ok(Duration::from_secs(within.parse::().map_err( + |_| ParserError::ParseError(format!("Can't parse {} as u64", within)), + )?)) +} + +fn parse_para_id(pair: Pair) -> Result { + let para_id_str = pair.into_inner().as_str(); + para_id_str + .parse::() + .map_err(|_| ParserError::ParseError(format!("Can't parse {} as u16", para_id_str))) +} + +fn parse_taget_value(pair: Pair) -> Result { + let target_str = pair.into_inner().as_str(); + target_str + .parse::() + .map_err(|_| ParserError::ParseError(format!("Can't parse {} as u64", target_str))) +} + +fn parse_comparison(pair: Pair) -> Result { + let mut inner_pairs = pair.into_inner(); + let op_rule = get_pair(&mut inner_pairs, "op_rule")?; + let op = match op_rule.as_rule() { + Rule::op_lte => ast::Operator::IsAtMost, + Rule::op_gte => ast::Operator::IsAtLeast, + Rule::op_lt => ast::Operator::IsBelow, + Rule::op_gt => ast::Operator::IsAbove, + Rule::op_eq => ast::Operator::Equal, + Rule::op_ineq => ast::Operator::NotEqual, + _ => { + return Err(ParserError::UnreachableRule(format!("{:?}", op_rule))); + } + }; + + let target_value = get_pair(&mut inner_pairs, "target_value")? + .as_str() + .parse::() + .map_err(|_| ParserError::ParseError("Can't parse as u64".to_string()))?; + + Ok(ast::Comparison { op, target_value }) +} + +fn parse_match_pattern_rule( + record: Pair, +) -> Result<(String, String, String, Option), ParserError> { + let mut pairs = record.into_inner(); + let name = parse_name(get_pair(&mut pairs, "name")?)?; + + let mut explicit_match_type = false; + + let pair = get_pair(&mut pairs, "match_type")?; + let match_type = if let Rule::match_type = pair.as_rule() { + explicit_match_type = true; + pair.as_str().to_owned() + } else { + String::from("regex") + }; + + let pattern_pair = if explicit_match_type { + get_pair(&mut pairs, "pattern")? + } else { + pair + }; + + let pattern = pattern_pair.as_str().trim_matches('"').to_owned(); + let timeout: Option = if let Some(within_rule) = pairs.next() { + Some(parse_within(within_rule)?) + } else { + None + }; + + Ok((name, match_type, pattern, timeout)) +} + +fn parse_custom_script_rule(record: Pair, is_js: bool) -> Result { + let mut pairs = record.into_inner(); + let node_name = parse_name(get_pair(&mut pairs, "name")?)?; + let file_path_str = get_pair(&mut pairs, "file_path")?.as_str(); + let file_path: PathBuf = file_path_str + .try_into() + .map_err(|_| errors::ParserError::ParseError(format!("Invalid path: {}", file_path_str)))?; + + let mut args: Option = None; + let mut cmp: Option = None; + let mut timeout = None; + + for inner_record in pairs { + match inner_record.as_rule() { + Rule::square_brackets_strings => { + args = Some(inner_record.as_str().to_owned()); + } + Rule::comparison => { + cmp = Some(parse_comparison(inner_record)?); + } + Rule::within => { + timeout = Some(parse_within(inner_record)?); + } + _ => { + return Err(ParserError::UnreachableRule( + inner_record.as_str().to_string(), + )); + } + } + } + + if is_js { + Ok(AssertionKind::CustomJs { + node_name, + file_path, + custom_args: args, + cmp, + timeout, + }) + } else { + Ok(AssertionKind::CustomSh { + node_name, + file_path, + custom_args: args, + cmp, + timeout, + }) + } +} + +/// Parse a `feature` file and return a `json string` +pub fn parse(unparsed_file: &str) -> Result { + let mut pairs = match ZombieNetParser::parse(Rule::file, unparsed_file) { + Ok(p) => p, + Err(e) => return Err(errors::ParserError::ParseError(e.to_string())), + }; + + let mut network: Option = None; + let mut creds: Option = None; + let mut description: Option = None; + let mut assertions: Vec = vec![]; + + let top_level_rule = if let Some(p) = pairs.next() { + p + } else { + return Err(ParserError::Unexpected(String::from( + "Invalid top level rule", + ))); + }; + + for record in top_level_rule.into_inner() { + let original_line = record.as_str().trim_end().to_string(); + + match record.as_rule() { + Rule::description => { + description = Some(record.into_inner().as_str().to_owned()); + } + Rule::network => { + network = Some(record.into_inner().as_str().to_owned()); + } + Rule::creds => { + let mut pairs = record.into_inner(); + creds = if let Some(creds_rule) = pairs.next() { + Some(creds_rule.into_inner().as_str().to_owned()) + } else { + Some(String::from("config")) + }; + } + Rule::is_up => { + // Pairs should be in order: + // name, [timeout] + let mut pairs = record.into_inner(); + let name = parse_name(get_pair(&mut pairs, "name")?)?; + + let timeout: Option = if let Some(within_rule) = pairs.next() { + Some(parse_within(within_rule)?) + } else { + None + }; + + let assertion = Assertion { + parsed: AssertionKind::IsUp { + node_name: name, + timeout, + }, + original_line, + }; + + assertions.push(assertion); + } + Rule::para_is_registered => { + // Pairs should be in order: + // name, para_id, [timeout] + let mut pairs = record.into_inner(); + let name = parse_name(get_pair(&mut pairs, "name")?)?; + let para_id = parse_para_id(get_pair(&mut pairs, "para_id")?)?; + + let timeout: Option = if let Some(within_rule) = pairs.next() { + Some(parse_within(within_rule)?) + } else { + None + }; + + let assertion = Assertion { + parsed: AssertionKind::ParaIsRegistered { + node_name: name, + para_id, + timeout, + }, + original_line, + }; + + assertions.push(assertion); + } + Rule::para_block_height => { + // Pairs should be in order: + // name, para_id, comparison, [timeout] + let mut pairs = record.into_inner(); + let name = parse_name(get_pair(&mut pairs, "name")?)?; + let para_id = parse_para_id(get_pair(&mut pairs, "para_id")?)?; + let comparison = parse_comparison(get_pair(&mut pairs, "comparison")?)?; + + let timeout: Option = if let Some(within_rule) = pairs.next() { + Some(parse_within(within_rule)?) + } else { + None + }; + + let assertion = Assertion { + parsed: AssertionKind::ParaBlockHeight { + node_name: name, + para_id, + op: comparison.op, + target_value: comparison.target_value, + timeout, + }, + original_line, + }; + + assertions.push(assertion); + } + Rule::para_runtime_upgrade => { + // Pairs should be in order: + // name, para_id, file_or_uri, [timeout] + let mut pairs = record.into_inner(); + let name = parse_name(get_pair(&mut pairs, "name")?)?; + let para_id = parse_para_id(get_pair(&mut pairs, "para_id")?)?; + let file_or_uri = get_pair(&mut pairs, "file_or_uri")?.as_str().to_string(); + let timeout: Option = if let Some(within_rule) = pairs.next() { + Some(parse_within(within_rule)?) + } else { + None + }; + + let assertion = Assertion { + parsed: AssertionKind::ParaRuntimeUpgrade { + node_name: name.to_owned(), + para_id, + file_or_uri, + timeout, + }, + original_line, + }; + + assertions.push(assertion); + } + Rule::para_runtime_dummy_upgrade => { + // Pairs should be in order: + // name, para_id, [timeout] + let mut pairs = record.into_inner(); + let name = parse_name(get_pair(&mut pairs, "name")?)?; + let para_id = parse_para_id(get_pair(&mut pairs, "para_id")?)?; + + let timeout: Option = if let Some(within_rule) = pairs.next() { + Some(parse_within(within_rule)?) + } else { + None + }; + + let assertion = Assertion { + parsed: AssertionKind::ParaRuntimeDummyUpgrade { + node_name: name.to_owned(), + para_id, + timeout, + }, + original_line, + }; + + assertions.push(assertion); + } + Rule::histogram => { + // Pairs should be in order: + // name, metric_name, cmp, buckets, [timeout] + let mut pairs = record.into_inner(); + let name = parse_name(get_pair(&mut pairs, "name")?)?; + let metric_name = get_pair(&mut pairs, "metric_name")?.as_str().to_string(); + let cmp_rule = get_pair(&mut pairs, "cmp_rule")?; + let cmp: ast::Comparison = match cmp_rule.as_rule() { + Rule::int => ast::Comparison { + op: ast::Operator::Equal, + target_value: parse_taget_value(cmp_rule)?, + }, + Rule::comparison => parse_comparison(cmp_rule)?, + _ => { + return Err(ParserError::UnreachableRule(pairs.as_str().to_string())); + } + }; + let buckets = get_pair(&mut pairs, "buckets")? + .as_str() + .trim_matches(|x| x == '[' || x == ']') + .split(',') + .map(|x| x.trim().trim_matches('"').to_string()) + .collect(); + + let timeout: Option = if let Some(within_rule) = pairs.next() { + Some(parse_within(within_rule)?) + } else { + None + }; + + let assertion = Assertion { + parsed: AssertionKind::Histogram { + node_name: name, + metric_name, + op: cmp.op, + target_value: cmp.target_value, + buckets, + timeout, + }, + original_line, + }; + + assertions.push(assertion); + } + Rule::report => { + // Pairs should be in order: + // name, metric_name, cmp, [timeout] + let mut pairs = record.into_inner(); + let name = parse_name(get_pair(&mut pairs, "name")?)?; + let metric_name = get_pair(&mut pairs, "metric_name")?.as_str().to_string(); + let cmp = parse_comparison(get_pair(&mut pairs, "cmp_rule")?)?; + let timeout: Option = if let Some(within_rule) = pairs.next() { + Some(parse_within(within_rule)?) + } else { + None + }; + + let assertion = Assertion { + parsed: AssertionKind::Report { + node_name: name.to_owned(), + metric_name, + op: cmp.op, + target_value: cmp.target_value, + timeout, + }, + original_line, + }; + + assertions.push(assertion); + } + Rule::log_match => { + let (name, match_type, pattern, timeout) = parse_match_pattern_rule(record)?; + + let assertion = Assertion { + parsed: AssertionKind::LogMatch { + node_name: name, + match_type, + pattern, + timeout, + }, + original_line, + }; + + assertions.push(assertion); + } + Rule::trace => { + // Pairs should be in order: + // name, span_id, pattern, [timeout] + let mut pairs = record.into_inner(); + let name = parse_name(get_pair(&mut pairs, "name")?)?; + let span_id = get_pair(&mut pairs, "span_id")?.as_str().to_string(); + let pattern = get_pair(&mut pairs, "pattern")?.as_str().to_string(); + + let timeout: Option = if let Some(within_rule) = pairs.next() { + Some(parse_within(within_rule)?) + } else { + None + }; + + let assertion = Assertion { + parsed: AssertionKind::Trace { + node_name: name.to_owned(), + span_id, + pattern, + timeout, + }, + original_line, + }; + + assertions.push(assertion); + } + Rule::system_event => { + let (name, match_type, pattern, timeout) = parse_match_pattern_rule(record)?; + + let assertion = Assertion { + parsed: AssertionKind::SystemEvent { + node_name: name, + match_type, + pattern, + timeout, + }, + original_line, + }; + + assertions.push(assertion); + } + Rule::custom_js => { + let parsed = parse_custom_script_rule(record, true)?; + let assertion = Assertion { + parsed, + original_line, + }; + + assertions.push(assertion); + } + Rule::custom_sh => { + let parsed = parse_custom_script_rule(record, false)?; + let assertion = Assertion { + parsed, + original_line, + }; + + assertions.push(assertion); + } + Rule::sleep => { + // Pairs should be in order: + // timeout + let mut pairs = record.into_inner(); + let seconds = get_pair(&mut pairs, "seconds")? + .as_str() + .parse::() + .map_err(|_| { + errors::ParserError::ParseError(String::from("Invalid secs value")) + })?; + + let assertion = Assertion { + parsed: AssertionKind::Sleep { + seconds: Some(Duration::from_secs(seconds)), + }, + original_line, + }; + + assertions.push(assertion); + } + Rule::pause => { + // Pairs should be in order: + // name + let mut pairs = record.into_inner(); + let name = parse_name(get_pair(&mut pairs, "name")?)?; + + let assertion = Assertion { + parsed: AssertionKind::Pause { node_name: name }, + original_line, + }; + + assertions.push(assertion); + } + Rule::resume => { + // Pairs should be in order: + // name + let mut pairs = record.into_inner(); + let name = parse_name(get_pair(&mut pairs, "name")?)?; + + let assertion = Assertion { + parsed: AssertionKind::Resume { node_name: name }, + original_line, + }; + + assertions.push(assertion); + } + Rule::restart => { + // Pairs should be in order: + // name + let mut pairs = record.into_inner(); + let name = parse_name(get_pair(&mut pairs, "name")?)?; + + let after: Option = if let Some(after_rule) = pairs.next() { + Some(Duration::from_secs(after_rule.as_str().parse().map_err( + |_| ParserError::ParseError(format!("Invalid after value, {}", after_rule)), + )?)) + } else { + None + }; + + let assertion = Assertion { + parsed: AssertionKind::Restart { + node_name: name, + after, + }, + original_line, + }; + + assertions.push(assertion); + } + Rule::EOI | Rule::comment => (), + _ => { + return Err(errors::ParserError::InvalidRule(record.as_str().to_owned())); + } + } + } + + if network.is_none() || creds.is_none() { + return Err(errors::ParserError::MissingFields(String::from( + "Missing Network/Creds field", + ))); + } + + // unwrap here should be face because of the above test. + let test_def = TestDefinition { + description, + network: network.unwrap(), + creds: creds.unwrap(), + assertions, + }; + + Ok(test_def) +} + +/// helper +fn get_pair<'a>( + pairs: &mut Pairs<'a, Rule>, + rule_name: &'a str, +) -> Result, ParserError> { + match pairs.next() { + Some(p) => Ok(p), + None => Err(ParserError::Unexpected(format!( + "Pair {} should exists", + rule_name + ))), + } +} diff --git a/crates/parser/src/tests.rs b/crates/parser/src/tests.rs new file mode 100644 index 000000000..7c8419b22 --- /dev/null +++ b/crates/parser/src/tests.rs @@ -0,0 +1,445 @@ +use super::*; + +const NETWORK: &str = "Network: ./a.toml"; +const CREDS: &str = "Creds: config"; + +#[test] +fn restart_parse_ok() { + let line: &str = "alice: restart after 60 seconds"; + let data = r#"{ + "description": null, + "network": "./a.toml", + "creds": "config", + "assertions": [ + { + "original_line": "alice: restart after 60 seconds", + "parsed": { + "fn": "Restart", + "args": { + "node_name": "alice", + "after": 60 + } + } + } + ] + }"#; + let t: TestDefinition = serde_json::from_str(data).unwrap(); + + let result = parse(&[NETWORK, CREDS, line].join("\n")).unwrap(); + assert_eq!(result, t); +} + +#[test] +fn is_up_parse_ok() { + let line: &str = "alice: is up within 5 secs"; + let data = r#"{ + "description": null, + "network": "./a.toml", + "creds": "config", + "assertions": [ + { + "original_line": "alice: is up within 5 secs", + "parsed": { + "fn": "IsUp", + "args": { + "node_name": "alice", + "timeout": 5 + } + } + } + ] + }"#; + let t: TestDefinition = serde_json::from_str(data).unwrap(); + + let result = parse(&[NETWORK, CREDS, line].join("\n")).unwrap(); + assert_eq!(result, t); +} + +#[test] +fn is_up_without_timeout_parse_ok() { + let line: &str = "alice: is up"; + let data = r#"{ + "description": null, + "network": "./a.toml", + "creds": "config", + "assertions": [ + { + "original_line": "alice: is up", + "parsed": { + "fn": "IsUp", + "args": { + "node_name": "alice", + "timeout": null + } + } + } + ] + }"#; + let t: TestDefinition = serde_json::from_str(data).unwrap(); + + let result = parse(&[NETWORK, CREDS, line].join("\n")).unwrap(); + assert_eq!(result, t); +} + +#[test] +fn para_is_registered_parse_ok() { + let line: &str = "alice: parachain 100 is registered within 225 seconds"; + let data = r#"{ + "description": null, + "network": "./a.toml", + "creds": "config", + "assertions": [ + { + "original_line": "alice: parachain 100 is registered within 225 seconds", + "parsed": { + "fn": "ParaIsRegistered", + "args": { + "node_name": "alice", + "para_id": 100, + "timeout": 225 + } + } + } + ] + }"#; + let t: TestDefinition = serde_json::from_str(data).unwrap(); + + let result = parse(&[NETWORK, CREDS, line].join("\n")).unwrap(); + assert_eq!(result, t); +} + +#[test] +fn histogram_parse_ok() { + let line: &str = r#"alice: reports histogram polkadot_pvf_preparation_time has at least 1 samples in buckets ["0.1", "0.5", "1", "2", "3", "10"] within 10 seconds"#; + let data = r#"{ + "description": null, + "network": "./a.toml", + "creds": "config", + "assertions": [ + { + "original_line": "alice: reports histogram polkadot_pvf_preparation_time has at least 1 samples in buckets [\"0.1\", \"0.5\", \"1\", \"2\", \"3\", \"10\"] within 10 seconds", + "parsed": { + "fn": "Histogram", + "args": { + "node_name": "alice", + "metric_name": "polkadot_pvf_preparation_time", + "op": "IsAtLeast", + "target_value": 1, + "buckets": ["0.1", "0.5", "1", "2", "3", "10"], + "timeout": 10 + } + } + } + ] + }"#; + let t: TestDefinition = serde_json::from_str(data).unwrap(); + + let result = parse(&[NETWORK, CREDS, line].join("\n")).unwrap(); + assert_eq!(result, t); +} + +#[test] +fn report_parse_ok() { + let line: &str = r#"eve: reports parachain_candidate_dispute_concluded{validity="invalid"} is 0 within 15 seconds"#; + let data = r#"{ + "description": null, + "network": "./a.toml", + "creds": "config", + "assertions": [ + { + "original_line": "eve: reports parachain_candidate_dispute_concluded{validity=\"invalid\"} is 0 within 15 seconds", + "parsed": { + "fn": "Report", + "args": { + "node_name": "eve", + "metric_name": "parachain_candidate_dispute_concluded{validity=\"invalid\"}", + "op": "Equal", + "target_value": 0, + "timeout": 15 + } + } + } + ] + }"#; + let t: TestDefinition = serde_json::from_str(data).unwrap(); + + let result = parse(&[NETWORK, CREDS, line].join("\n")).unwrap(); + assert_eq!(result, t); +} + +#[test] +fn para_dummy_upgrade_parse_ok() { + let line: &str = r#"alice: parachain 100 perform dummy upgrade within 200 seconds"#; + let data = r#"{ + "description": null, + "network": "./a.toml", + "creds": "config", + "assertions": [ + { + "original_line": "alice: parachain 100 perform dummy upgrade within 200 seconds", + "parsed": { + "fn": "ParaRuntimeDummyUpgrade", + "args": { + "node_name": "alice", + "para_id": 100, + "timeout": 200 + } + } + } + ] + }"#; + let t: TestDefinition = serde_json::from_str(data).unwrap(); + + let result = parse(&[NETWORK, CREDS, line].join("\n")).unwrap(); + assert_eq!(result, t); +} + +#[test] +fn para_upgrade_parse_ok() { + let line: &str = + r#"alice: parachain 100 perform upgrade with ./some.wasm.compact within 200 seconds"#; + let data = r#"{ + "description": null, + "network": "./a.toml", + "creds": "config", + "assertions": [ + { + "original_line": "alice: parachain 100 perform upgrade with ./some.wasm.compact within 200 seconds", + "parsed": { + "fn": "ParaRuntimeUpgrade", + "args": { + "node_name": "alice", + "para_id": 100, + "file_or_uri": "./some.wasm.compact", + "timeout": 200 + } + } + } + ] + }"#; + let t: TestDefinition = serde_json::from_str(data).unwrap(); + + let result = parse(&[NETWORK, CREDS, line].join("\n")).unwrap(); + assert_eq!(result, t); +} + +#[test] +fn log_match_parse_ok() { + let line: &str = r#"alice: log line contains "Imported #12" within 20 seconds"#; + let data = r#"{ + "description": null, + "network": "./a.toml", + "creds": "config", + "assertions": [ + { + "original_line": "alice: log line contains \"Imported #12\" within 20 seconds", + "parsed": { + "fn": "LogMatch", + "args": { + "node_name": "alice", + "match_type": "regex", + "pattern": "Imported #12", + "timeout": 20 + } + } + } + ] + }"#; + let t: TestDefinition = serde_json::from_str(data).unwrap(); + + let result = parse(&[NETWORK, CREDS, line].join("\n")).unwrap(); + assert_eq!(result, t); +} + +#[test] +fn log_match_parse_glob_ok() { + let line: &str = r#"alice: log line contains glob "Imported #12" within 20 seconds"#; + let data = r#"{ + "description": null, + "network": "./a.toml", + "creds": "config", + "assertions": [ + { + "original_line": "alice: log line contains glob \"Imported #12\" within 20 seconds", + "parsed": { + "fn": "LogMatch", + "args": { + "node_name": "alice", + "match_type": "glob", + "pattern": "Imported #12", + "timeout": 20 + } + } + } + ] + }"#; + let t: TestDefinition = serde_json::from_str(data).unwrap(); + + let result = parse(&[NETWORK, CREDS, line].join("\n")).unwrap(); + assert_eq!(result, t); +} + +#[test] +fn log_match_glob_parse_ok() { + let line: &str = r#"alice: log line matches glob "*rted #1*" within 10 seconds"#; + let data = r#"{ + "description": null, + "network": "./a.toml", + "creds": "config", + "assertions": [ + { + "original_line": "alice: log line matches glob \"*rted #1*\" within 10 seconds", + "parsed": { + "fn": "LogMatch", + "args": { + "node_name": "alice", + "match_type": "glob", + "pattern": "*rted #1*", + "timeout": 10 + } + } + } + ] + }"#; + let t: TestDefinition = serde_json::from_str(data).unwrap(); + + let result = parse(&[NETWORK, CREDS, line].join("\n")).unwrap(); + assert_eq!(result, t); +} + +#[test] +fn trace_parse_ok() { + let line: &str = r#"alice: trace with traceID 94c1501a78a0d83c498cc92deec264d9 contains ["answer-chunk-request", "answer-chunk-request"]"#; + let data = r#"{ + "description": null, + "network": "./a.toml", + "creds": "config", + "assertions": [ + { + "original_line": "alice: trace with traceID 94c1501a78a0d83c498cc92deec264d9 contains [\"answer-chunk-request\", \"answer-chunk-request\"]", + "parsed": { + "fn": "Trace", + "args": { + "node_name": "alice", + "span_id": "94c1501a78a0d83c498cc92deec264d9", + "pattern": "[\"answer-chunk-request\", \"answer-chunk-request\"]", + "timeout": null + } + } + } + ] + }"#; + let t: TestDefinition = serde_json::from_str(data).unwrap(); + + let result = parse(&[NETWORK, CREDS, line].join("\n")).unwrap(); + assert_eq!(result, t); +} + +#[test] +fn system_event_parse_ok() { + let line: &str = r#"alice: system event contains "A candidate was included" within 20 seconds"#; + let data = r#"{ + "description": null, + "network": "./a.toml", + "creds": "config", + "assertions": [ + { + "original_line": "alice: system event contains \"A candidate was included\" within 20 seconds", + "parsed": { + "fn": "SystemEvent", + "args": { + "node_name": "alice", + "match_type": "regex", + "pattern": "A candidate was included", + "timeout": 20 + } + } + } + ] + }"#; + let t: TestDefinition = serde_json::from_str(data).unwrap(); + + let result = parse(&[NETWORK, CREDS, line].join("\n")).unwrap(); + assert_eq!(result, t); +} + +#[test] +fn custom_js_parse_ok() { + let line: &str = r#"alice: js-script ./0008-custom.js within 200 seconds"#; + let data = r#"{ + "description": null, + "network": "./a.toml", + "creds": "config", + "assertions": [ + { + "original_line": "alice: js-script ./0008-custom.js within 200 seconds", + "parsed": { + "fn": "CustomJs", + "args": { + "node_name": "alice", + "file_path": "./0008-custom.js", + "custom_args": null, + "timeout": 200 + } + } + } + ] + }"#; + let t: TestDefinition = serde_json::from_str(data).unwrap(); + + let result = parse(&[NETWORK, CREDS, line].join("\n")).unwrap(); + assert_eq!(result, t); +} + +#[test] +fn custom_sh_parse_ok() { + let line: &str = r#"alice: run ./0008-custom.sh within 200 seconds"#; + let data = r#"{ + "description": null, + "network": "./a.toml", + "creds": "config", + "assertions": [ + { + "original_line": "alice: run ./0008-custom.sh within 200 seconds", + "parsed": { + "fn": "CustomSh", + "args": { + "node_name": "alice", + "file_path": "./0008-custom.sh", + "custom_args": null, + "timeout": 200 + } + } + } + ] + }"#; + let t: TestDefinition = serde_json::from_str(data).unwrap(); + + let result = parse(&[NETWORK, CREDS, line].join("\n")).unwrap(); + assert_eq!(result, t); +} + +#[test] +fn is_up_parse_err() { + let result = parse("alice: is upp"); + assert!(result.is_err()); +} + +/// Header test +#[test] +fn parse_header_ok() { + let result = parse(&[NETWORK, CREDS, "alice: is up"].join("\n")); + assert!(result.is_ok()); +} + +#[test] +fn parse_header_with_description_ok() { + let result = parse(&["Description: Some", NETWORK, CREDS, "alice: is up"].join("\n")); + assert!(result.is_ok()); +} + +#[test] +fn parse_header_err() { + let result = parse(&[CREDS, NETWORK, "alice: is up"].join("\n")); + assert!(result.is_err()); +} diff --git a/crates/parser/src/zombienet.pest b/crates/parser/src/zombienet.pest new file mode 100644 index 000000000..596b94b36 --- /dev/null +++ b/crates/parser/src/zombienet.pest @@ -0,0 +1,104 @@ +/// Helpers +WHITESPACE = _{ " " | "\t" | "\r" | "\n" } +/// matches permited node's names +name = @{ (ASCII_ALPHANUMERIC | "-")+ } +/// matched any integer +int = @{ "-" ? ~ ("0" | '1'..'9' ~ '0'..'9' * ) } +/// matches any char +char = @{ 'a'..'z' | 'A'..'Z' | "." | "_" | "/" } +/// matches anything between 2 double quotes +double_quoted_string = @{ "\"" ~ (!("\"") ~ ANY)* ~ "\""} +/// matches anything between 2 single quotes +single_quoted_string = @{ "\'" ~ (!("\'") ~ ANY)* ~ "\'"} +/// matches anything between 2 backquotes\backticks +backquoted_quoted_string = @{ "`" ~ (!("`") ~ ANY)* ~ "`"} +/// matches a single colon +colon = { ":" } +/// matches uri +uri = @{ "http" ~ "s"? ~ "://" ~ (ASCII_ALPHANUMERIC| "/" | "-" | "_" | ".")+ } +/// matches any file path +file_path = @{ "."{0,2} ~ "/" ~ (ASCII_ALPHANUMERIC | "-" | "_" | "." )+ } +/// matches prometheus metrics labels +metric_key_value = @{ (ASCII_ALPHANUMERIC | "_" | "-")+ ~ "=" ~ double_quoted_string } +/// matches Jaeger span ids +span_id = { ASCII_HEX_DIGIT{32} } + +/// METRICS +block_height = { "blockheight" | "block height" | "best block" } +finalized_height = { "finalised height" | "finalised block" } +peers_count = { "peers count" | "peers" } + +metric_name = @{ (block_height | finalized_height | peers_count | (ASCII_ALPHANUMERIC | "_" )+) ~ ("{" ~ metric_key_value ~ "}")? } +square_brackets_strings = { + "[" ~ double_quoted_string ~ ("," ~ double_quoted_string)* ~ "]" +} +match_type = { "regex" | "glob" } + +/// OPERATORS +op_lte = { "<=" } +op_gte = { ">=" | "is at least" | "at least" } +op_lt = { "<" | "is lower than" } +op_gt = { ">" | "is greater than" | "greater than" } +op_eq = { "==" | "=" | "equals" | "is equal to" | "is" } +op_ineq = { "!=" } +comparison = { (op_lte | op_gte | op_gt | op_lt | op_eq | op_ineq) ~ int+ } + +// commons +node_name = { name ~ colon } +seconds = _{ "seconds"|"secs"|"s" } +within = { "within" ~ int+ ~ seconds } +parachain = { "parachain" ~ int+ } + +// CONFIG +inner_description = @{(!NEWLINE ~ ANY)+} +description = { "Description:" ~ inner_description } +network = { "Network:" ~ file_path } +creds = { "Creds:" ~ ("config" | file_path) } + +// ASSERTIONS +is_up = { node_name ~ "is up" ~ within? } +para_is_registered = { node_name ~ parachain ~ "is registered" ~ within? } +para_block_height = { node_name ~ parachain ~ "block height" ~ comparison ~ within? } +para_runtime_upgrade = { node_name ~ parachain ~ "perform upgrade with" ~ ( uri | file_path ) ~ within? } +para_runtime_dummy_upgrade = { node_name ~ parachain ~ "perform dummy upgrade" ~ within? } +histogram = { node_name ~ "reports histogram" ~ metric_name ~ "has" ~ (comparison | int+) ~ "samples in buckets" ~ square_brackets_strings ~ within? } +report = { node_name ~ "reports" ~ metric_name ~ comparison ~ within? } +log_match = { node_name ~ "log line" ~ ("contains"|"matches") ~ match_type? ~ double_quoted_string ~ within? } +trace = { node_name ~ "trace with traceID" ~ span_id ~ "contains" ~ square_brackets_strings ~ within? } +system_event = { node_name ~ "system event" ~ ("contains"|"matches") ~ match_type? ~ double_quoted_string ~ within? } +custom_js = { node_name ~ "js-script" ~ file_path ~ ("with" ~ square_brackets_strings)? ~ ( "return" ~ comparison )? ~ within? } +custom_sh = { node_name ~ "run" ~ file_path ~ ("with" ~ square_brackets_strings)? ~ ( "return" ~ comparison )? ~ within? } + +/// COMMANDS +sleep = { "sleep" ~ int+ ~ seconds } +pause = { node_name ~ "pause" } +resume = { node_name ~ "resume" } +restart = { node_name ~ "restart" ~ ("after" ~ int+ ~ seconds)? } + +/// COMMENTS +comment = ${ ("#" | "//") ~ (!NEWLINE ~ ANY)+ } + + +file = { SOI ~ ( + description? ~ + network ~ + creds + ) ~ ( + comment | + is_up | + para_is_registered | + para_block_height | + para_runtime_upgrade | + para_runtime_dummy_upgrade | + histogram | + report | + log_match | + trace | + system_event | + custom_js | + custom_sh | + sleep | + pause | + resume | + restart + )* ~ NEWLINE* ~ EOI } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 057d99a11..4dc805c51 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,9 +9,11 @@ "version": "1.2.71", "license": "GPL-3.0-or-later", "dependencies": { + "@parity/zombienet-dsl-parser-wrapper": "^0.1.2", "@polkadot/api": "^9.2.4", "@polkadot/keyring": "^10.1.6", "@polkadot/util-crypto": "^10.1.6", + "@types/chai": "^4.3.3", "axios": "^0.27.2", "chai": "^4.3.4", "cli-table3": "^0.6.2", @@ -239,6 +241,11 @@ "node": ">= 8" } }, + "node_modules/@parity/zombienet-dsl-parser-wrapper": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@parity/zombienet-dsl-parser-wrapper/-/zombienet-dsl-parser-wrapper-0.1.2.tgz", + "integrity": "sha512-jUH/GImvHoJjdYmZK42DYAQEo1vuPdNZFD4rLc5R16bRid+jszOrVKYH3a/u4+oXEo5tCIKAsFTLEZtR8o0o3Q==" + }, "node_modules/@polkadot/api": { "version": "9.4.2", "resolved": "https://registry.npmjs.org/@polkadot/api/-/api-9.4.2.tgz", @@ -839,6 +846,11 @@ "@types/node": "*" } }, + "node_modules/@types/chai": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.3.tgz", + "integrity": "sha512-hC7OMnszpxhZPduX+m+nrx+uFoLkWOMiR4oa/AZF3MuSETYTZmFfJAHqZEM8MVlvfG7BEUcgvtwoCTxBp6hm3g==" + }, "node_modules/@types/fs-extra": { "version": "9.0.13", "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", @@ -4977,6 +4989,11 @@ "fastq": "^1.6.0" } }, + "@parity/zombienet-dsl-parser-wrapper": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@parity/zombienet-dsl-parser-wrapper/-/zombienet-dsl-parser-wrapper-0.1.2.tgz", + "integrity": "sha512-jUH/GImvHoJjdYmZK42DYAQEo1vuPdNZFD4rLc5R16bRid+jszOrVKYH3a/u4+oXEo5tCIKAsFTLEZtR8o0o3Q==" + }, "@polkadot/api": { "version": "9.4.2", "resolved": "https://registry.npmjs.org/@polkadot/api/-/api-9.4.2.tgz", @@ -5445,6 +5462,11 @@ "@types/node": "*" } }, + "@types/chai": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.3.tgz", + "integrity": "sha512-hC7OMnszpxhZPduX+m+nrx+uFoLkWOMiR4oa/AZF3MuSETYTZmFfJAHqZEM8MVlvfG7BEUcgvtwoCTxBp6hm3g==" + }, "@types/fs-extra": { "version": "9.0.13", "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", diff --git a/package.json b/package.json index a09dbbc34..f96d78030 100644 --- a/package.json +++ b/package.json @@ -24,9 +24,11 @@ "docs": "npm run docs:build && npm run docs:deploy" }, "dependencies": { + "@parity/zombienet-dsl-parser-wrapper": "^0.1.3", "@polkadot/api": "^9.2.4", "@polkadot/keyring": "^10.1.6", "@polkadot/util-crypto": "^10.1.6", + "@types/chai": "^4.3.3", "axios": "^0.27.2", "chai": "^4.3.4", "cli-table3": "^0.6.2", diff --git a/src/cli.ts b/src/cli.ts index 9c0819d5b..6a2dcd0d4 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -14,6 +14,7 @@ import { ParachainConfig, PL_ConfigType, PolkadotLaunchConfig, + TestDefinition, } from "./types"; import { askQuestion, getCredsFilePath, readNetworkConfig } from "./utils/fs"; @@ -28,7 +29,11 @@ const DEFAULT_CUMULUS_COLLATOR_URL = // const DEFAULT_ADDER_COLLATOR_URL = // "https://gitlab.parity.io/parity/mirrors/polkadot/-/jobs/1769497/artifacts/raw/artifacts/adder-collator"; import { decorators } from "./utils/colors"; + +import parser from "@parity/zombienet-dsl-parser-wrapper"; +import { Environment } from "nunjucks"; import { convertBytes, getFilePathNameExt } from "./utils/misc"; +import { RelativeLoader } from "./utils/nunjucks-relative-loader"; interface OptIf { [key: string]: { name: string; url?: string; size?: string }; @@ -126,6 +131,16 @@ const latestPolkadotReleaseURL = async ( } }; +function getTestNameFromFileName(testFile: string): string { + const fileWithOutExt = testFile.split(".")[0]; + const fileName: string = fileWithOutExt.split("/").pop() || ""; + const parts = fileName.split("-"); + const name = parts[0].match(/\d/) + ? parts.slice(1).join(" ") + : parts.join(" "); + return name; +} + // Convert functions // Read the input file async function readInputFile( @@ -434,8 +449,26 @@ async function test( opts.provider && AVAILABLE_PROVIDERS.includes(opts.provider) ? opts.provider : "kubernetes"; + + const configBasePath = path.dirname(testFile); + const env = new Environment(new RelativeLoader([configBasePath])); + const temmplateContent = fs.readFileSync(testFile).toString(); + const content = env.renderString(temmplateContent, process.env); + + const testName = getTestNameFromFileName(testFile); + + let testDef: TestDefinition; + try { + testDef = JSON.parse(parser.parse_to_json(content)); + } catch (e) { + console.log(e); + process.exit(1); + } + await run( - testFile, + configBasePath, + testName, + testDef, providerToUse, inCI, opts.spawnConcurrency, diff --git a/src/jsapi-helpers/events.ts b/src/jsapi-helpers/events.ts index a60b00ce7..e60f22f43 100644 --- a/src/jsapi-helpers/events.ts +++ b/src/jsapi-helpers/events.ts @@ -28,7 +28,7 @@ export async function findPatternInSystemEventSubscription( event.data.forEach((data: any, index: any) => { eventString += `${types[index].type};${data.toString()}`; }); - + debug(eventString); return re.test(eventString); }); diff --git a/src/jsapi-helpers/index.ts b/src/jsapi-helpers/index.ts index 01583ee7e..2c5cb48c1 100644 --- a/src/jsapi-helpers/index.ts +++ b/src/jsapi-helpers/index.ts @@ -8,7 +8,7 @@ import { import { findPatternInSystemEventSubscription } from "./events"; import { paraGetBlockHeight, paraIsRegistered } from "./parachain"; -async function connect(apiUrl: string, types: any): Promise { +async function connect(apiUrl: string, types?: any): Promise { const provider = new WsProvider(apiUrl); const api = new ApiPromise({ provider, types }); await api.isReady; diff --git a/src/test-runner/assertions.ts b/src/test-runner/assertions.ts new file mode 100644 index 000000000..7799f474e --- /dev/null +++ b/src/test-runner/assertions.ts @@ -0,0 +1,422 @@ +import { ApiPromise, Keyring } from "@polkadot/api"; +import { assert, expect } from "chai"; +import { JSDOM } from "jsdom"; +import minimatch from "minimatch"; +import path from "path"; +import { BackchannelMap } from "."; +import { DEFAULT_INDIVIDUAL_TEST_TIMEOUT } from "../constants"; +import { + chainCustomSectionUpgrade, + chainUpgradeFromLocalFile, + chainUpgradeFromUrl, + connect, + findPatternInSystemEventSubscription, + validateRuntimeCode, +} from "../jsapi-helpers"; +import { Network } from "../network"; +import { FnArgs } from "../types"; +import { decorators } from "../utils/colors"; +import { isValidHttpUrl } from "../utils/misc"; +const utilCrypto = require("@polkadot/util-crypto"); + +// helper +function toChaiComparator(op: string): string { + return op.charAt(0).toLocaleLowerCase() + op.slice(1); +} + +const comparators: { [key: string]: Function } = { + Equal: assert.equal, + NotEqual: assert.notEqual, + IsAbove: assert.isAbove, + IsAtLeast: assert.isAtLeast, + IsBelow: assert.isBelow, + IsAtMost: assert.isAtMost, +}; + +const IsUp = ({ node_name, timeout }: FnArgs) => { + timeout = timeout || DEFAULT_INDIVIDUAL_TEST_TIMEOUT; + return async (network: Network) => { + const nodes = network.getNodes(node_name!); + const results = await Promise.all( + nodes.map((node) => + node.getMetric("process_start_time_seconds", "isAtLeast", 1, timeout), + ), + ); + const AllNodeUps = results.every(Boolean); + expect(AllNodeUps).to.be.ok; + }; +}; + +const Report = ({ + node_name, + metric_name, + target_value, + op, + timeout, +}: FnArgs) => { + const comparatorFn = comparators[op!]; + return async (network: Network, backchannelMap: BackchannelMap) => { + const nodes = network.getNodes(node_name!); + const results = await Promise.all( + nodes.map((node) => + node.getMetric( + metric_name!, + toChaiComparator(op!), + target_value!, + timeout || DEFAULT_INDIVIDUAL_TEST_TIMEOUT, + ), + ), + ); + + for (const value of results) { + comparatorFn(value as number, target_value as number); + } + }; +}; + +const Histogram = ({ + node_name, + metric_name, + target_value, + buckets, + op, + timeout, +}: FnArgs) => { + const comparatorFn = comparators[op!]; + return async (network: Network, backchannelMap: BackchannelMap) => { + const nodes = network.getNodes(node_name!); + const results = await Promise.all( + nodes.map((node) => + node.getHistogramSamplesInBuckets( + metric_name!, + buckets!, + target_value!, + timeout || DEFAULT_INDIVIDUAL_TEST_TIMEOUT, + ), + ), + ); + + for (const value of results) { + comparatorFn(value, target_value); + } + }; +}; + +const Trace = ({ node_name, span_id, pattern }: FnArgs) => { + const spanNames = pattern! + .split(",") + .map((x) => x.replaceAll('"', "").trim()); + return async (network: Network, backchannelMap: BackchannelMap) => { + const nodes = network.getNodes(node_name!); + const results = await Promise.all( + nodes.map((node) => + node.getSpansByTraceId(span_id!, network.tracing_collator_url!), + ), + ); + + for (const value of results) { + assert.includeOrderedMembers(value, spanNames); + } + }; +}; + +const LogMatch = ({ node_name, pattern, match_type, timeout }: FnArgs) => { + const isGlob = (match_type && match_type.trim() === "glob") || false; + + return async (network: Network) => { + const nodes = network.getNodes(node_name!); + const results = await Promise.all( + nodes.map((node) => node.findPattern(pattern!, isGlob, timeout)), + ); + + const found = results.every(Boolean); + expect(found).to.be.ok; + }; +}; + +const SystemEvent = ({ node_name, pattern, match_type, timeout }: FnArgs) => { + const isGlob = (match_type && match_type.trim() === "glob") || false; + + return async (network: Network) => { + const node = network.node(node_name!); + const api: ApiPromise = await connect(node.wsUri); + const re = isGlob ? minimatch.makeRe(pattern!) : new RegExp(pattern!, "ig"); + const found = await findPatternInSystemEventSubscription( + api, + re as RegExp, + timeout || DEFAULT_INDIVIDUAL_TEST_TIMEOUT, + ); + api.disconnect(); + + expect(found).to.be.ok; + }; +}; + +// Customs +const CustomJs = ({ + node_name, + file_path, + custom_args, + op, + target_value, + timeout, +}: FnArgs) => { + timeout = timeout || DEFAULT_INDIVIDUAL_TEST_TIMEOUT; + const comparatorFn = comparators[op!]; + + return async ( + network: Network, + backchannelMap: BackchannelMap, + configBasePath: string, + ) => { + const networkInfo = { + tmpDir: network.tmpDir, + chainSpecPath: network.chainSpecFullPath, + relay: network.relay.map((node) => { + const { name, wsUri, prometheusUri, userDefinedTypes } = node; + return { name, wsUri, prometheusUri, userDefinedTypes }; + }), + paras: Object.keys(network.paras).reduce((memo: any, paraId: any) => { + memo[paraId] = { chainSpecPath: network.paras[paraId].chainSpecPath }; + memo[paraId].nodes = network.paras[paraId].nodes.map((node) => { + return { ...node }; + }); + return memo; + }, {}), + nodesByName: Object.keys(network.nodesByName).reduce( + (memo: any, nodeName) => { + const { name, wsUri, prometheusUri, userDefinedTypes, parachainId } = + network.nodesByName[nodeName]; + memo[nodeName] = { name, wsUri, prometheusUri, userDefinedTypes }; + if (parachainId) memo[nodeName].parachainId = parachainId; + return memo; + }, + {}, + ), + }; + + const nodes = network.getNodes(node_name!); + const call_args = custom_args + ? custom_args === "" + ? [] + : custom_args.split("with ").slice(1)[0].replaceAll('"', "").split(",") + : []; + + // const fileTestPath = path.dirname(testFile); + const resolvedJsFilePath = path.resolve(configBasePath, file_path!); + + // shim with jsdom + const dom = new JSDOM( + "", + ); + (global as any).window = dom.window; + (global as any).document = dom.window.document; + (global as any).zombie = { + ApiPromise, + Keyring, + util: utilCrypto, + connect, + }; + const jsScript = await import(resolvedJsFilePath); + + let values; + try { + const resp: any = await Promise.race([ + Promise.all( + nodes.map((node) => jsScript.run(node.name, networkInfo, call_args)), + ), + new Promise((resolve) => + setTimeout(() => { + const err = new Error( + `Timeout(${timeout}), "custom-js ${file_path!} within ${timeout} secs" didn't complete on time.`, + ); + return resolve(err); + }, timeout! * 1000), + ), + ]); + if (resp instanceof Error) throw new Error(resp as any); + else values = resp; + } catch (err: any) { + console.log( + `\n\t ${decorators.red(`Error running script: ${file_path!}`)}`, + ); + console.log(`\t\t ${err.message}\n`); + throw new Error(err); + } + + // remove shim + (global as any).window = undefined; + (global as any).document = undefined; + (global as any).zombie = undefined; + + if (target_value) { + for (const value of values) { + comparatorFn(value, target_value); + } + } else { + // test don't have matching output + expect(true).to.be.ok; + } + }; +}; + +const CustomSh = ({ + node_name, + file_path, + custom_args, + op, + target_value, + timeout, +}: FnArgs) => { + timeout = timeout || DEFAULT_INDIVIDUAL_TEST_TIMEOUT; + const comparatorFn = comparators[op!]; + + return async ( + network: Network, + backchannelMap: BackchannelMap, + configBasePath: string, + ) => { + try { + // const fileTestPath = path.dirname(testFile); + const resolvedShFilePath = path.resolve(configBasePath, file_path!); + + const nodes = network.getNodes(node_name!); + const call_args = custom_args + ? custom_args === "" + ? [] + : custom_args + .split("with ") + .slice(1)[0] + .replaceAll('"', "") + .split(",") + : []; + + const results = await Promise.all( + nodes.map((node) => node.run(resolvedShFilePath, call_args, timeout)), + ); + + if (comparatorFn && target_value !== undefined) { + for (const value of results) { + comparatorFn(value, target_value); + } + } + + // all the commands run successfully + expect(true).to.be.ok; + } catch (err: any) { + console.log( + `\n\t ${decorators.red(`Error running script: ${file_path!}`)}`, + ); + console.log(`\t\t ${err.message}\n`); + throw new Error(err); + } + }; +}; + +// Paras +const ParaIsRegistered = ({ node_name, para_id, timeout }: FnArgs) => { + return async (network: Network) => { + const nodes = network.getNodes(node_name!); + const results = await Promise.all( + nodes.map((node) => node.parachainIsRegistered(para_id!, timeout)), + ); + + const parachainIsRegistered = results.every(Boolean); + expect(parachainIsRegistered).to.be.ok; + }; +}; + +const ParaBlockHeight = ({ + node_name, + para_id, + target_value, + op, + timeout, +}: FnArgs) => { + timeout = timeout || DEFAULT_INDIVIDUAL_TEST_TIMEOUT; + return async (network: Network) => { + const nodes = network.getNodes(node_name!); + const comparatorFn = comparators[op!]; + + const results = await Promise.all( + nodes.map((node) => + node.parachainBlockHeight(para_id!, target_value!, timeout), + ), + ); + for (const value of results) { + comparatorFn(value, target_value!); + } + }; +}; + +const ParaRuntimeUpgrade = ({ + node_name, + para_id, + file_or_uri, + timeout, +}: FnArgs) => { + timeout = timeout || DEFAULT_INDIVIDUAL_TEST_TIMEOUT; + return async ( + network: Network, + backchannelMap: BackchannelMap, + configBasePath: string, + ) => { + let node = network.node(node_name!); + let api: ApiPromise = await connect(node.wsUri); + let hash; + + if (isValidHttpUrl(file_or_uri!)) { + hash = await chainUpgradeFromUrl(api, file_or_uri!); + } else { + // const fileTestPath = path.dirname(testFile); + const resolvedJsFilePath = path.resolve(configBasePath, file_or_uri!); + hash = await chainUpgradeFromLocalFile(api, resolvedJsFilePath); + } + + // validate in a node of the relay chain + api.disconnect(); + const { wsUri, userDefinedTypes } = network.relay[0]; + api = await connect(wsUri, userDefinedTypes); + const valid = await validateRuntimeCode(api, para_id!, hash, timeout); + api.disconnect(); + + expect(valid).to.be.ok; + }; +}; + +const ParaRuntimeDummyUpgrade = ({ node_name, para_id, timeout }: FnArgs) => { + timeout = timeout || DEFAULT_INDIVIDUAL_TEST_TIMEOUT; + return async ( + network: Network, + backchannelMap: BackchannelMap, + configBasePath: string, + ) => { + const collator = network.paras[para_id!].nodes[0]; + let node = network.node(collator.name); + let api: ApiPromise = await connect(node.wsUri); + const hash = await chainCustomSectionUpgrade(api); + + // validate in the : of the relay chain + node = network.node(node_name!); + api = await connect(node.wsUri); + const valid = await validateRuntimeCode(api, para_id!, hash, timeout); + api.disconnect(); + + expect(valid).to.be.ok; + }; +}; + +export default { + IsUp, + Report, + Histogram, + Trace, + LogMatch, + SystemEvent, + CustomJs, + CustomSh, + ParaBlockHeight, + ParaIsRegistered, + ParaRuntimeUpgrade, + ParaRuntimeDummyUpgrade, +}; diff --git a/src/test-runner/commnads.ts b/src/test-runner/commnads.ts new file mode 100644 index 000000000..6b929fcb2 --- /dev/null +++ b/src/test-runner/commnads.ts @@ -0,0 +1,55 @@ +const chai = require("chai"); + +import { BackchannelMap } from "."; +import { Network } from "../network"; +import { FnArgs } from "../types"; +import { sleep } from "../utils/misc"; + +const { expect } = chai; + +const Pause = ({ node_name }: FnArgs) => { + return async (network: Network, backchannelMap: BackchannelMap) => { + const nodes = network.getNodes(node_name!); + const results = await Promise.all(nodes.map((node) => node.pause())); + + for (const value of results) { + expect(value).to.be.ok; + } + }; +}; + +const Resume = ({ node_name }: FnArgs) => { + return async (network: Network, backchannelMap: BackchannelMap) => { + const nodes = network.getNodes(node_name!); + const results = await Promise.all(nodes.map((node) => node.resume())); + + for (const value of results) { + expect(value).to.be.ok; + } + }; +}; +const Restart = ({ node_name, after }: FnArgs) => { + after = after || 5; // at least 1 seconds + return async (network: Network, backchannelMap: BackchannelMap) => { + const nodes = network.getNodes(node_name!); + const results = await Promise.all(nodes.map((node) => node.restart(after))); + + for (const value of results) { + expect(value).to.be.ok; + } + }; +}; +const Sleep = ({ seconds }: FnArgs) => { + seconds = seconds || 1; + return async () => { + await sleep(seconds! * 1000); + expect(true).to.be.ok; + }; +}; + +export default { + Pause, + Restart, + Resume, + Sleep, +}; diff --git a/src/test-runner/index.ts b/src/test-runner/index.ts index 37ee23ad6..0eb140326 100644 --- a/src/test-runner/index.ts +++ b/src/test-runner/index.ts @@ -1,54 +1,32 @@ const chai = require("chai"); -import { ApiPromise, Keyring } from "@polkadot/api"; import fs from "fs"; -import minimatch from "minimatch"; import Mocha from "mocha"; import path from "path"; -import { - DEFAULT_GLOBAL_TIMEOUT, - DEFAULT_INDIVIDUAL_TEST_TIMEOUT, -} from "../constants"; +import { DEFAULT_GLOBAL_TIMEOUT } from "../constants"; import { Network, rebuildNetwork } from "../network"; import { Providers } from "../providers/"; -import { LaunchConfig } from "../types"; +import { LaunchConfig, TestDefinition } from "../types"; import { decorators } from "../utils/colors"; import { readNetworkConfig } from "../utils/fs"; -import { getLokiUrl, isValidHttpUrl, sleep } from "../utils/misc"; -const utilCrypto = require("@polkadot/util-crypto"); +import { getLokiUrl, sleep } from "../utils/misc"; +import assertions from "./assertions"; +import commnads from "./commnads"; import zombie from "../"; -const { - connect, - chainUpgradeFromUrl, - chainUpgradeFromLocalFile, - chainCustomSectionUpgrade, - validateRuntimeCode, - findPatternInSystemEventSubscription, -} = require("../jsapi-helpers"); const debug = require("debug")("zombie::test-runner"); -const { assert, expect } = chai; const { Test, Suite } = Mocha; const mocha = new Mocha(); -import { JSDOM } from "jsdom"; -import { Environment } from "nunjucks"; -import { RelativeLoader } from "../utils/nunjucks-relative-loader"; - -interface TestDefinition { - networkConfig: string; - creds: string; - description?: string; - assertions: string[]; -} - export interface BackchannelMap { [propertyName: string]: any; } export async function run( - testFile: string, + configBasePath: string, + testName: string, + testDef: TestDefinition, provider: string, inCI: boolean = false, concurrency: number = 1, @@ -56,22 +34,15 @@ export async function run( ) { let network: Network; let backchannelMap: BackchannelMap = {}; - // read test file - const testDef = parseTestFile(testFile); - const testName = getTestNameFromFileName(testFile); + let suiteName: string = testName; if (testDef.description) suiteName += `( ${testDef.description} )`; // read network file - let config: LaunchConfig; - if (fs.existsSync(testDef.networkConfig)) { - config = readNetworkConfig(testDef.networkConfig); - } else { - // the path is relative to the test file - const fileTestPath = path.dirname(testFile); - const resolvedFilePath = path.resolve(fileTestPath, testDef.networkConfig); - config = readNetworkConfig(resolvedFilePath); - } + let networkConfigFilePath = fs.existsSync(testDef.network) + ? testDef.network + : path.resolve(configBasePath, testDef.network); + const config: LaunchConfig = readNetworkConfig(networkConfigFilePath); // set the provider if (!config.settings) @@ -236,11 +207,20 @@ export async function run( }); for (const assertion of testDef.assertions) { - const testFn = parseAssertionLine(assertion); - if (!testFn) continue; + let generator = fns[assertion.parsed.fn as keyof Fns]; + debug(generator); + + if (!generator) { + console.log( + `\n\t ${decorators.red("Invalid fn generator:" + assertion.parsed.fn)}`, + ); + process.exit(1); + } + + let testFn = generator(assertion.parsed.args); const test = new Test( - assertion, - async () => await testFn(network, backchannelMap, testFile), + assertion.original_line, + async () => await testFn(network, backchannelMap, configBasePath), ); suite.addTest(test); test.timeout(0); @@ -280,673 +260,9 @@ const exitMocha = (code: number) => { done(); }; -// REGEX -// Node general -const isUpRegex = new RegExp( - /^(([\w-]+): is up)+( within (\d+) (seconds|secs|s)?)?$/i, -); - -// parachains -const parachainIsRegistered = new RegExp( - /^(([\w-]+): parachain (\d+) is registered)+( within (\d+) (seconds|secs|s)?)?$/i, -); -const parachainBlockHeight = new RegExp( - /^(([\w-]+): parachain (\d+) block height is (equal to|equals|=|==|greater than|>|at least|>=|lower than|<)? *(\d+))+( within (\d+) (seconds|secs|s))?$/i, -); -const chainUpgradeRegex = new RegExp( - /^(([\w-]+): parachain (\d+) perform upgrade with (.*?))+( within (\d+) (seconds|secs|s)?)$/i, -); -const chainDummyUpgradeRegex = new RegExp( - /^(([\w-]+): parachain (\d+) perform dummy upgrade)+( within (\d+) (seconds|secs|s)?)$/i, -); - -// Metrics - histograms -// e.g alice: reports histogram pvf_execution_time has at last X samples in buckets ["3", "4", "6", "+Inf"] -const isHistogram = new RegExp( - /^(([\w-]+): reports histogram (.*?) has (equal to|equals|=|==|greater than|>|at least|>=|lower than|<)? *(\d+) samples in buckets \[(.+)\])+( within (\d+) (seconds|secs|s))?$/i, -); - -// Metrics -const isReports = new RegExp( - /^(([\w-]+): reports (.*?) is (equal to|equals|=|==|greater than|>|at least|>=|lower than|<)? *(\d+))+( within (\d+) (seconds|secs|s))?$/i, -); - -// Logs assertion -const assertLogLineRegex = new RegExp( - /^(([\w-]+): log line (contains|matches)( regex| glob)? "(.+)")+( within (\d+) (seconds|secs|s))?$/i, -); - -// Tracing assertion -// alice: trace with traceID contains ["name", "name2",...] -const isTracing = new RegExp( - /^(([\w-]+): trace with traceID (.*?) contains \[(.+)\])+( within (\d+) (seconds|secs|s))?$/i, -); - -// system events -const assertSystemEventRegex = new RegExp( - /^(([\w-]+): system event (contains|matches)( regex| glob)? "(.+)")+( within (\d+) (seconds|secs|s))?$/i, -); - -// Custom js-script -const assertCustomJsRegex = new RegExp( - /^([\w-]+): js-script (\.{0,2}\/.*\.[\w]+)( with \"[\w ,-/]+\")?( return is (equal to|equals|=|==|greater than|>|at least|>=|lower than|<)? *(\d+))?( within (\d+) (seconds|secs|s))?$/i, -); - -// Run command in the node -const assertCustomShInNode = new RegExp( - /^([\w-]+): run (\.{0,2}\/.*\.[\w]+)( with \"[\w \,\-\/:.]+\")?( return is (equal to|equals|=|==|greater than|>|at least|>=|lower than|<)? *(\d+))?( within (\d+) (seconds|secs|s))?$/i, -); - -// Backchannel -// alice: wait for name and use as X within 30s -const backchannelWait = new RegExp( - /^([\w-]+): wait for (.*?) and use as (.*?) within (\d+) (seconds|secs|s)?$/i, -); - -// Alice: ensure var:X is used -const isEnsure = new RegExp(/^([\w-]+): ensure var:([\w]+) is used$/i); - -// Commands -const sleepRegex = new RegExp(/^sleep *(\d+) (seconds|secs|s)?$/i); -const restartRegex = new RegExp( - /^(([\w-]+): restart)+( after (\d+) (seconds|secs|s))?$/i, -); -const pauseRegex = new RegExp(/^([\w-]+): pause$/i); -const resumeRegex = new RegExp(/^([\w-]+): resume$/i); - -function parseAssertionLine(assertion: string) { - // Matchs - let m: string[] | null; - - m = parachainIsRegistered.exec(assertion); - if (m && m[2] && m[3]) { - const nodeName = m[2]; - const parachainId = parseInt(m[3], 10); - let t: number; - if (m[5]) t = parseInt(m[5], 10); - - return async (network: Network) => { - const timeout: number | undefined = t; - const nodes = network.getNodes(nodeName); - const results = await Promise.all( - nodes.map((node) => node.parachainIsRegistered(parachainId, timeout)), - ); - - const parachainIsRegistered = results.every(Boolean); - expect(parachainIsRegistered).to.be.ok; - }; - } - - m = parachainBlockHeight.exec(assertion); - if (m && m[2] && m[3] && m[4] && m[5]) { - let t: number; - const nodeName = m[2]; - const parachainId = parseInt(m[3], 10); - const comparatorFn = getComparatorFn(m[4] || ""); - const targetValue = parseInt(m[5]); - if (m[7]) t = parseInt(m[7], 10); - - return async (network: Network) => { - const timeout: number | undefined = t; - const nodes = network.getNodes(nodeName); - - const results = await Promise.all( - nodes.map((node) => - node.parachainBlockHeight(parachainId, targetValue, timeout), - ), - ); - for (const value of results) { - assert[comparatorFn](value, targetValue); - } - }; - } - - m = isUpRegex.exec(assertion); - if (m && m[2] !== null) { - let t: number; - const nodeName = m[2]; - if (m[4]) t = parseInt(m[4], 10); - return async (network: Network) => { - const timeout: number | undefined = t; - const nodes = network.getNodes(nodeName); - const results = await Promise.all( - nodes.map((node) => - node.getMetric("process_start_time_seconds", "isAtLeast", 1, timeout), - ), - ); - const AllNodeUps = results.every(Boolean); - expect(AllNodeUps).to.be.ok; - }; - } - - m = isHistogram.exec(assertion); - if (m && m[2] && m[3] && m[5]) { - let t: number; - const nodeName = m[2]; - const metricName = m[3]; - const comparatorFn = getComparatorFn(m[4] || ""); - const targetValue = parseInt(m[5]); - const buckets = m[6].split(",").map((x) => x.replaceAll('"', "").trim()); - if (m[8]) t = parseInt(m[8], 10); - return async (network: Network, backchannelMap: BackchannelMap) => { - const timeout: number | undefined = t; - const nodes = network.getNodes(nodeName); - const results = await Promise.all( - nodes.map((node) => - node.getHistogramSamplesInBuckets( - metricName, - buckets, - targetValue, - timeout, - ), - ), - ); - - for (const value of results) { - assert[comparatorFn](value, targetValue); - } - }; - } - - // alice: trace with traceID contains ["name", "name2",...] - m = isTracing.exec(assertion); - if (m && m[2] && m[3] && m[4]) { - let t: number; - const nodeName = m[2]; - const traceId = m[3]; - const spanNames = m[4].split(",").map((x) => x.replaceAll('"', "").trim()); - if (m[8]) t = parseInt(m[8], 10); - return async (network: Network, backchannelMap: BackchannelMap) => { - const _timeout: number | undefined = t; - const nodes = network.getNodes(nodeName); - const results = await Promise.all( - nodes.map((node) => - node.getSpansByTraceId(traceId, network.tracing_collator_url!), - ), - ); - - for (const value of results) { - assert.includeOrderedMembers(value, spanNames); - } - }; - } - - m = isReports.exec(assertion); - if (m && m[2] && m[3] && m[5]) { - let t: number; - const nodeName = m[2]; - const metricName = m[3]; - const comparatorFn = getComparatorFn(m[4] || ""); - const targetValue = parseInt(m[5]); - if (m[7]) t = parseInt(m[7], 10); - return async (network: Network, backchannelMap: BackchannelMap) => { - const timeout: number | undefined = t; - const nodes = network.getNodes(nodeName); - const results = await Promise.all( - nodes.map((node) => - node.getMetric(metricName, comparatorFn, targetValue, timeout), - ), - ); - - for (const value of results) { - assert[comparatorFn](value, targetValue); - } - }; - } - - m = assertLogLineRegex.exec(assertion); - if (m && m[2] && m[5]) { - let t: number; - const nodeName = m[2]; - const pattern = m[5]; - const isGlob = (m[4] && m[4].trim() === "glob") || false; - if (m[7]) t = parseInt(m[7], 10); - - return async (network: Network) => { - const timeout: number | undefined = t; - const nodes = network.getNodes(nodeName); - const results = await Promise.all( - nodes.map((node) => node.findPattern(pattern, isGlob, timeout)), - ); - - const found = results.every(Boolean); - expect(found).to.be.ok; - }; - } - - m = assertSystemEventRegex.exec(assertion); - if (m && m[2] && m[5]) { - const nodeName = m[2]; - const pattern = m[5]; - const isGlob = (m[4] && m[4].trim() === "glob") || false; - const t = m[7] ? parseInt(m[7], 10) : DEFAULT_INDIVIDUAL_TEST_TIMEOUT; - - return async (network: Network) => { - const timeout: number | undefined = t; - const node = network.node(nodeName); - const api: ApiPromise = await connect(node.wsUri); - const re = isGlob ? minimatch.makeRe(pattern) : new RegExp(pattern, "ig"); - const found = await findPatternInSystemEventSubscription( - api, - re, - timeout, - ); - api.disconnect(); - - expect(found).to.be.ok; - }; - } - - m = assertCustomJsRegex.exec(assertion); - if (m && m[1] && m[2]) { - const nodeName = m[1]; - const jsFile = m[2]; - const withArgs = m[3] ? m[3] : ""; - const comparatorFn = getComparatorFn(m[5] || ""); - let targetValue: string | number | undefined = m[6]; - const timeout = m[8] ? parseInt(m[8], 10) : DEFAULT_INDIVIDUAL_TEST_TIMEOUT; - - return async ( - network: Network, - backchannelMap: BackchannelMap, - testFile: string, - ) => { - const networkInfo = { - tmpDir: network.tmpDir, - chainSpecPath: network.chainSpecFullPath, - relay: network.relay.map((node) => { - const { name, wsUri, prometheusUri, userDefinedTypes } = node; - return { name, wsUri, prometheusUri, userDefinedTypes }; - }), - paras: Object.keys(network.paras).reduce((memo: any, paraId: any) => { - memo[paraId] = { chainSpecPath: network.paras[paraId].chainSpecPath }; - memo[paraId].nodes = network.paras[paraId].nodes.map((node) => { - return { ...node }; - }); - return memo; - }, {}), - nodesByName: Object.keys(network.nodesByName).reduce( - (memo: any, nodeName) => { - const { - name, - wsUri, - prometheusUri, - userDefinedTypes, - parachainId, - } = network.nodesByName[nodeName]; - memo[nodeName] = { name, wsUri, prometheusUri, userDefinedTypes }; - if (parachainId) memo[nodeName].parachainId = parachainId; - return memo; - }, - {}, - ), - }; - - const nodes = network.getNodes(nodeName); - const args = - withArgs === "" - ? [] - : withArgs.split("with ").slice(1)[0].replaceAll('"', "").split(","); - const fileTestPath = path.dirname(testFile); - const resolvedJsFilePath = path.resolve(fileTestPath, jsFile); - - // shim with jsdom - const dom = new JSDOM( - "", - ); - (global as any).window = dom.window; - (global as any).document = dom.window.document; - (global as any).zombie = { - ApiPromise, - Keyring, - util: utilCrypto, - connect, - }; - const jsScript = await import(resolvedJsFilePath); - - let values; - try { - const resp: any = await Promise.race([ - Promise.all( - nodes.map((node) => jsScript.run(node.name, networkInfo, args)), - ), - new Promise((resolve) => - setTimeout(() => { - const err = new Error( - `Timeout(${timeout}), "custom-js ${jsFile} within ${timeout} secs" didn't complete on time.`, - ); - return resolve(err); - }, timeout * 1000), - ), - ]); - if (resp instanceof Error) throw new Error(resp as any); - else values = resp; - } catch (err: any) { - console.log( - `\n\t ${decorators.red(`Error running script: ${jsFile}`)}`, - ); - console.log(`\t\t ${err.message}\n`); - throw new Error(err); - } - - // remove shim - (global as any).window = undefined; - (global as any).document = undefined; - (global as any).zombie = undefined; - - if (targetValue) { - if (comparatorFn !== "equals") - targetValue = parseInt(targetValue as string, 10); - for (const value of values) { - assert[comparatorFn](value, targetValue); - } - } else { - // test don't have matching output - expect(true).to.be.ok; - } - }; - } - - m = assertCustomShInNode.exec(assertion); - if (m && m[1] && m[2]) { - const nodeName = m[1]; - const shFile = m[2]; - const withArgs = m[3] ? m[3] : ""; - const comparatorFn = getComparatorFn(m[5] || ""); - let targetValue: string | number | undefined = m[6]; - const t = m[8] ? parseInt(m[8], 10) : DEFAULT_INDIVIDUAL_TEST_TIMEOUT; - - return async ( - network: Network, - backchannelMap: BackchannelMap, - testFile: string, - ) => { - try { - const timeout: number | undefined = t; - const fileTestPath = path.dirname(testFile); - const resolvedShFilePath = path.resolve(fileTestPath, shFile); - - const nodes = network.getNodes(nodeName); - const args = - withArgs === "" - ? [] - : withArgs - .split("with ") - .slice(1)[0] - .replaceAll('"', "") - .split(","); - const results = await Promise.all( - nodes.map((node) => node.run(resolvedShFilePath, args, timeout)), - ); - - if (comparatorFn && targetValue !== undefined) { - for (const value of results) { - assert[comparatorFn](value, targetValue); - } - } - - // all the commands run successfully - expect(true).to.be.ok; - } catch (err: any) { - console.log( - `\n\t ${decorators.red(`Error running script: ${shFile}`)}`, - ); - console.log(`\t\t ${err.message}\n`); - throw new Error(err); - } - }; - } - - m = backchannelWait.exec(assertion); - if (m && m[1] && m[2] && m[3]) { - let timeout: number; - const backchannelKey = m[2]; - const backchannelMapKey = m[3]; // for use locally after with `var:KEY` - if (m[4]) timeout = parseInt(m[4]); - return async (network: Network, backchannelMap: BackchannelMap) => { - try { - const value = await network.getBackchannelValue( - backchannelKey, - timeout, - ); - backchannelMap[backchannelMapKey] = value; - // return ok - assert.equal(0, 0); - } catch (err) { - throw new Error(`Error getting ${backchannelKey} from backchannel`); - } - }; - } - - m = isEnsure.exec(assertion); - if (m && m[1] && m[2]) { - const backchannelMapKey = m[2]; // for use locally after with `var:KEY` - return async (network: Network, backchannelMap: BackchannelMap) => { - const defined = backchannelMap[backchannelMapKey] !== undefined; - expect(defined).to.be.ok; - }; - } - - m = restartRegex.exec(assertion); - if (m && m[2]) { - const nodeName = m[2]; - let t: number; - if (m[4]) t = parseInt(m[4], 10); - return async (network: Network, backchannelMap: BackchannelMap) => { - const timeout: number | undefined = t; - const nodes = network.getNodes(nodeName); - const results = await Promise.all( - nodes.map((node) => node.restart(timeout)), - ); - - for (const value of results) { - expect(value).to.be.ok; - } - }; - } - - m = pauseRegex.exec(assertion); - if (m && m[1]) { - const nodeName = m[1]; - return async (network: Network, backchannelMap: BackchannelMap) => { - const nodes = network.getNodes(nodeName); - const results = await Promise.all(nodes.map((node) => node.pause())); - - for (const value of results) { - expect(value).to.be.ok; - } - }; - } - - m = resumeRegex.exec(assertion); - if (m && m[1]) { - const nodeName = m[1]; - return async (network: Network, backchannelMap: BackchannelMap) => { - const nodes = network.getNodes(nodeName); - const results = await Promise.all(nodes.map((node) => node.resume())); - - for (const value of results) { - expect(value).to.be.ok; - } - }; - } - - m = sleepRegex.exec(assertion); - if (m && m[1]) { - const timeout = parseInt(m[1], 10); - return async () => { - await sleep(timeout * 1000); - expect(true).to.be.ok; - }; - } - - m = chainUpgradeRegex.exec(assertion); - if (m && m[2]) { - const nodeName = m[2]; - const parachainId = parseInt(m[3], 10); - const upgradeFileOrUrl = m[4]; - let timeout: number; - if (m[6]) timeout = parseInt(m[6], 10); - - return async ( - network: Network, - backchannelMap: BackchannelMap, - testFile: string, - ) => { - let node = network.node(nodeName); - let api: ApiPromise = await connect(node.wsUri); - let hash; - - if (isValidHttpUrl(upgradeFileOrUrl)) { - hash = await chainUpgradeFromUrl(api, upgradeFileOrUrl); - } else { - const fileTestPath = path.dirname(testFile); - const resolvedJsFilePath = path.resolve(fileTestPath, upgradeFileOrUrl); - hash = await chainUpgradeFromLocalFile(api, resolvedJsFilePath); - } - - // validate in a node of the relay chain - api.disconnect(); - const { wsUri, userDefinedTypes } = network.relay[0]; - api = await connect(wsUri, userDefinedTypes); - const valid = await validateRuntimeCode(api, parachainId, hash, timeout); - api.disconnect(); - - expect(valid).to.be.ok; - }; - } - - m = chainDummyUpgradeRegex.exec(assertion); - if (m && m[2]) { - const nodeName = m[2]; - const parachainId = parseInt(m[3], 10); - let timeout: number; - if (m[5]) timeout = parseInt(m[5], 10); - - return async ( - network: Network, - backchannelMap: BackchannelMap, - testFile: string, - ) => { - const collator = network.paras[parachainId].nodes[0]; - let node = network.node(collator.name); - let api: ApiPromise = await connect(node.wsUri); - const hash = await chainCustomSectionUpgrade(api); - - // validate in the : of the relay chain - node = network.node(nodeName); - api = await connect(node.wsUri); - const valid = await validateRuntimeCode(api, parachainId, hash, timeout); - api.disconnect(); - - expect(valid).to.be.ok; - }; - } - - // if we can't match let produce a fail test - return async (network: Network) => { - console.log( - `\n\t ${decorators.red("Failed to match, please check syntax.")}`, - ); - assert.equal(0, 1); - }; -} - -function getComparatorFn(comparator: string) { - let fn; - switch (comparator.trim()) { - case "equals": - case "=": - case "==": - case "equal to": - fn = "equal"; - break; - case "greater than": - case ">": - fn = "isAbove"; - break; - case "at least": - case ">=": - fn = "isAtLeast"; - break; - case "lower than": - case "<": - fn = "isBelow"; - break; - default: //default - fn = "equal"; - break; - } - - return fn; -} - -function getTestNameFromFileName(testFile: string): string { - const fileWithOutExt = testFile.split(".")[0]; - const fileName: string = fileWithOutExt.split("/").pop() || ""; - const parts = fileName.split("-"); - const name = parts[0].match(/\d/) - ? parts.slice(1).join(" ") - : parts.join(" "); - return name; -} - -function parseTestFile(testFile: string): TestDefinition { - let testDefinition: TestDefinition | undefined = undefined; - - const configBasePath = path.dirname(testFile); - const env = new Environment(new RelativeLoader([configBasePath])); - const temmplateContent = fs.readFileSync(testFile).toString(); - const content = env.renderString(temmplateContent, process.env); - - let networkConfig: string = ""; - let description: string = ""; - let creds: string = ""; - const assertions = []; - - for (let line of content.split("\n")) { - line = line.trim(); - if (line[0] === "#" || line.length === 0) continue; // skip comments and empty lines; - let parts = line.split(":"); - if (parts.length < 2 && !line.includes("sleep")) continue; // bad line - switch (parts[0].toLocaleLowerCase()) { - case "network": - networkConfig = parts[1].trim(); - break; - case "creds": - creds = parts[1].trim(); - break; - case "description": - description = parts[1].trim(); - break; - default: - assertions.push(line); - break; - } - } - - const required = ["Network", "Creds"]; - const missing = required.filter((value) => { - if (value === "Network" && !networkConfig) return true; - if (value === "Creds" && !creds) return true; - }); - - if (missing.length > 0) - throw new Error( - `Invalid test definition, missing: ${required.join( - ",", - )}. file: ${testFile}`, - ); - - testDefinition = { - networkConfig, - creds, - assertions, - description, - }; - - // extra check - if (!testDefinition) - throw new Error(`Invalid test definition, file: ${testFile}`); - return testDefinition; -} +// Generators registry +type Fns = { [key: string]: Function }; +const fns: Fns = { + ...assertions, + ...commnads, +}; diff --git a/src/types.d.ts b/src/types.d.ts index 77a0061a4..697e4f2d1 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -265,6 +265,39 @@ export interface MultiAddressByNode { [key: string]: string; } +export interface TestDefinition { + network: string; + creds: string; + description?: string; + assertions: Assertion[]; +} + +export interface Assertion { + original_line: string; + parsed: { + fn: string; + args: FnArgs; + }; +} + +export interface FnArgs { + node_name?: string; + para_id?: number; + timeout?: number; + target_value?: number; + metric_name?: string; + buckets?: string[]; + span_id?: string; + op?: string; + pattern?: string; + match_type?: string; + file_path?: string; + custom_args?: string; + file_or_uri?: string; + after?: number; + seconds?: number; +} + // Config interfaces interface PL_NodesConfig { name: string; diff --git a/tests/0007-events.zndsl b/tests/0007-events.zndsl index d74841c47..654065874 100644 --- a/tests/0007-events.zndsl +++ b/tests/0007-events.zndsl @@ -8,4 +8,4 @@ bob: is up alice: reports block height is at least 10 within 200 seconds alice: system event contains "A candidate was included" within 20 seconds alice: system event matches glob "*was backed*" within 10 seconds -alice: system event matches "\"paraId\":[0-9]+" within 10 seconds \ No newline at end of file +alice: system event matches "paraId(.)*[0-9]+" within 10 seconds