diff --git a/.github/workflows/build_and_release.yml b/.github/workflows/build_and_release.yml new file mode 100644 index 0000000..663769e --- /dev/null +++ b/.github/workflows/build_and_release.yml @@ -0,0 +1,90 @@ +name: Build and release test helpers +on: + push: + branches: + - 'main' + - 'port-test-helpers' +env: + GH_TOKEN: ${{ github.token }} +permissions: + contents: write # allows creating releases, uploading assets +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v5.0.0 + + - name: Install rustup toolchain + uses: dtolnay/rust-toolchain@v1 + with: + toolchain: "stable-x86_64-unknown-linux-gnu" + + - name: Cache Cargo registry + build + uses: Swatinem/rust-cache@v2.8.0 + + - name: Extract crate version + id: version + run: | + VERSION=$(grep '^version' Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/') + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - name: Get short commit hash + id: commit + run: | + HASH="${GITHUB_SHA::4}" + echo "hash=${HASH}" >> $GITHUB_OUTPUT + + - name: Build release binary + run: | + cargo build --release -p test-helpers --target-dir ./build/test-helpers + + - name: Prepare artifacts + id: prepare + run: | + mkdir -p dist + VERSION="${{ steps.version.outputs.version }}" + HASH="${{ steps.commit.outputs.hash }}" + # All binaries + PARENT_DIR='./build' + for dir in "$PARENT_DIR"/*/; do + dir_name=$(basename "$dir") + dist_name="${dir_name}@${VERSION}-${HASH}" + mkdir -p "./dist/$dist_name" + + # find exec files + exe_files=() + src_dir="$PARENT_DIR/$dir_name/release" + for f in "$src_dir"/*; do [[ -x $f && -f $f ]] && exe_files+=( "$f" ); done + + # Copy exe files + if (( ${#exe_files[@]} > 0 )); then + cp -r "${exe_files[@]}" "./dist/$dist_name/" + fi + + # Copy .so files + if find "$src_dir" -maxdepth 1 -type f -name "*.so" | grep -q .; then + cp -r $src_dir/*.so "./dist/$dist_name" + fi + + # Generate .tar.gz files + if [ -n "$(ls -A ./dist/$dist_name/ 2>/dev/null)" ]; then + echo "files to tar: " + ls -A ./dist/$dist_name/ + tar -czf ./dist/$dist_name.tar.gz -C ./dist/$dist_name/ $(echo ./dist/$dist_name/* | xargs -n1 basename) + fi + done + # Export dist path as step output + echo "dist_dir=$(pwd)/dist" >> $GITHUB_OUTPUT + echo "version_tag=${VERSION}-${HASH}" >> $GITHUB_OUTPUT + + - name: Create release + run: | + VERSION_TAG="${{steps.prepare.outputs.version_tag}}" + DIST_DIR="${{steps.prepare.outputs.dist_dir}}" + gh release create $VERSION_TAG $DIST_DIR/*.tar.gz -t "Release $VERSION_TAG" -n "Release binaries for test-helpers" + + - name: Print artifact info + run: | + echo "Artifact successfully built ✅" + echo " Version: test-helpers@${{ steps.version.outputs.version }}-${{ steps.commit.outputs.hash }}" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5278f8a..f32a01d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,9 +6,6 @@ on: jobs: test: runs-on: ubuntu-latest - defaults: - run: - working-directory: ./test-helpers steps: - name: Checkout repository uses: actions/checkout@v5.0.0 diff --git a/.gitignore b/.gitignore index ad67955..db856f5 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,7 @@ target # Generated by cargo mutants # Contains mutation testing data **/mutants.out*/ - +builds # RustRover # JetBrains specific template is maintained in a separate JetBrains.gitignore that can # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..46f144c --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,728 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +dependencies = [ + "windows-sys 0.60.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.60.2", +] + +[[package]] +name = "anyhow" +version = "1.0.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "backtrace" +version = "0.3.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "bitflags" +version = "2.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "cfg-if" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "env_filter" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "io-uring" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" +dependencies = [ + "bitflags", + "cfg-if", + "libc", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jiff" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde", +] + +[[package]] +name = "jiff-static" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "libc" +version = "0.2.175" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.59.0", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" + +[[package]] +name = "rustc-demangle" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.143" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "syn" +version = "2.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "test-helpers" +version = "0.1.0" +dependencies = [ + "anyhow", + "bytes", + "env_logger", + "http-body-util", + "httparse", + "hyper", + "hyper-util", + "log", + "serde", + "serde_json", + "tokio", +] + +[[package]] +name = "tokio" +version = "1.47.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +dependencies = [ + "backtrace", + "bytes", + "io-uring", + "libc", + "mio", + "pin-project-lite", + "slab", + "socket2", + "tokio-macros", + "windows-sys 0.59.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.3", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..eef5327 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "test-helpers" +version = "0.1.0" +edition = "2024" + +[dependencies] +env_logger = "0.11.8" +log = "0.4.27" +http-body-util = "0.1" +anyhow = "1.0.98" +hyper = {version = "1.7.0", features = ["server", "http1"]} +bytes = "1.10.1" +serde = {version = "1.0.219", features = ["derive"]} +tokio = {version = "1.47.1", features = ["rt", "rt-multi-thread", "net", "macros", "io-util"]} +hyper-util = {version = "0.1.16", features = ["tokio"]} +serde_json = "1.0.143" +httparse = "1.10.1" + + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..1d1c487 --- /dev/null +++ b/Makefile @@ -0,0 +1,5 @@ +echo: + cargo build --release --bin echo + +jsonth: + cargo build --release --bin jsonth \ No newline at end of file diff --git a/README.MD b/README.MD new file mode 100644 index 0000000..8e95492 --- /dev/null +++ b/README.MD @@ -0,0 +1,3 @@ +# Ooniprobe Test Helpers + +Implementation of some tests helpers \ No newline at end of file diff --git a/src/bin/echo.rs b/src/bin/echo.rs new file mode 100644 index 0000000..179b92d --- /dev/null +++ b/src/bin/echo.rs @@ -0,0 +1,30 @@ +use log::{error, info}; +use test_helpers::helper_runner::{read_port, run_tcp_server}; +use tokio::{io, net::TcpStream}; + +#[tokio::main] +async fn main() { + let port = read_port("8000"); + run_tcp_server("echoth", &port, handle_tcp_echo).await; +} + +async fn handle_tcp_echo(mut stream: TcpStream) { + // For development, an easy way to test this function is starting the server and using telnet + // to send some data and check if the data comes back properly + // + // ``` + // telnet localhost 8000 + // ``` + info!("Connection received"); + + let (mut reader, mut writer) = stream.split(); + + // Note that this function will get stucked here until the client closes the connection, + // continuosly sending the data it receives. This is expected. + let result = io::copy(&mut reader, &mut writer).await; + match result { + Ok(0) => info!("Connection closed"), + Ok(n) => info!("Received {n} bytes in total"), + Err(e) => error!("Error processing request: {e}"), + } +} diff --git a/src/bin/jsonth.rs b/src/bin/jsonth.rs new file mode 100644 index 0000000..4644084 --- /dev/null +++ b/src/bin/jsonth.rs @@ -0,0 +1,208 @@ +use std::collections::HashMap; +use std::default::Default; + +use anyhow::Result; +use bytes::Bytes; +use http_body_util::Full; +use httparse::{EMPTY_HEADER, Status, parse_headers}; +use hyper::{Request, Response, StatusCode, header}; +use hyper::{server::conn::http1, service::service_fn}; +use hyper_util::rt::TokioIo; +use log::{error, info}; +use serde::Serialize; +use test_helpers::helper_runner::{read_port, run_tcp_server}; +use tokio::net::TcpStream; + +#[tokio::main] +async fn main() { + let port = read_port("8000"); + run_tcp_server("json_helper", &port, handle_json_helper).await; +} + +#[derive(Serialize, Default, Clone)] +pub struct JsonResponse { + request_line: String, + headers_dict: HashMap>, +} + +/** +Process the HTTP Request Line and the Request Headers and +returns them in a JSON datastructure in the order +we received them. + +The returned JSON dict looks like so: + +```json +{ +'request_line': +'GET / HTTP/1.1', +'headers_dict' : {'Accept': ['application/json', 'text/plain']} +} +``` +*/ +async fn handle_json_helper(socket: TcpStream) { + // Note that hyper can't give us the request line, so we parse it before + // going to hyper + let response = parse_line_and_headers(&socket).await; + + // Parse headers using hyper to parse the request. + let io = TokioIo::new(socket); + if let Err(e) = http1::Builder::new() + .preserve_header_case(true) + .serve_connection( + io, + service_fn(move |req| send_json_response(response.clone(), req)), + ) + .await + { + error!("Could not serve request: {e}") + } +} + +/** + Parse headers and send response using hyper + + hyper can't give you the request line, so we parse the request line manually + before calling this handler +*/ +async fn send_json_response( + response: Result, + _request: Request, +) -> Result>, hyper::Error> { + let response = match response { + Ok(s) => s, + Err(e) => { + return make_error_response( + format!("Couldn't parse line or headers: {e}"), + StatusCode::INTERNAL_SERVER_ERROR, + ); + } + }; + + log_response(&response); + make_response(&response) +} + +async fn parse_line_and_headers(socket: &TcpStream) -> Result { + // Recommended size for uri is 8000 octets, longest part of the request line + // https://www.rfc-editor.org/rfc/rfc9110.html#name-uri-references + // We also add more headspace to parse headers as well + let mut buffer = [0u8; 4 * 8192]; + + // use peek to avoid consuming from the stream + match socket.peek(&mut buffer).await { + Ok(0) => Err("Connection closed unexpectedly".to_string()), + Ok(n) => { + // Parse request line + let request_line = parse_line(&buffer[..n]).await?; + + // Start of headers is len of request line + 2 due to the \r\n terminator + let start = request_line.len() + 2; + + // Parse headers from buffer, starting after the request line: + let headers_dict = parse_headers_list(&buffer[start..])?; + + Ok(JsonResponse { + request_line, + headers_dict, + }) + } + Err(e) => Err(format!("Unable to read from socket: {e}")), + } +} + +async fn parse_line(buffer: &[u8]) -> Result { + // Parse bytes as str + let line = match std::str::from_utf8(buffer) { + Ok(v) => v, + Err(e) => return Err(format!("Unable to parse request line: {e}")), + }; + + line.split("\r\n") + .next() + .map(|s| s.to_string()) + .ok_or("Bad http request".to_string()) +} + +fn parse_headers_list(buffer: &[u8]) -> Result>, String> { + let mut headers_dict: HashMap> = HashMap::new(); + let mut headers_buff = [EMPTY_HEADER; 100]; + + let headers = match parse_headers(buffer, &mut headers_buff) { + Ok(Status::Complete((_, headers))) => headers, + Ok(Status::Partial) => { + return Err("Buffer too small to contain headers".into()); + } + Err(e) => return Err(e.to_string()), + }; + + // Parse header values + for header in headers.iter() { + let entry = headers_dict.entry(header.name.into()).or_default(); + + // Note that headers are not usually utf8, but every ascii header is valid utf8. + // We will note enforce it here, but hyper will when parsing the request + let value = match std::str::from_utf8(header.value) { + Ok(v) => v.to_string(), + Err(e) => { + return Err(format!("Error parsing header, non-utf8 header found: {e}")); + } + }; + + entry.push(value); + } + + Ok(headers_dict) +} + +fn make_response(resp: &JsonResponse) -> Result>, hyper::Error> { + let json = serde_json::to_vec(&resp).expect("Couldn't serialize response"); + Ok(Response::builder() + .status(StatusCode::OK) + .header(header::CONTENT_TYPE, "application/json") + .body(Full::new(Bytes::from(json))) + .unwrap()) +} + +#[derive(Serialize)] +pub struct ErrorResponse { + message: String, +} + +fn make_error_response( + message: String, + status: StatusCode, +) -> Result>, hyper::Error> { + let resp = ErrorResponse { message }; + let json = serde_json::to_vec(&resp).expect("Couldn't serialize response"); + + Ok(Response::builder() + .status(status) + .header(header::CONTENT_TYPE, "application/json") + .body(Full::new(Bytes::from(json))) + .unwrap()) +} + +fn log_response(resp: &JsonResponse) { + // request line, user agent, host + let mut user_agent = ""; + for (key, value) in &resp.headers_dict { + if key.to_lowercase() == "user-agent" { + user_agent = value[0].as_str(); + break; + } + } + + let mut host = ""; + for (key, value) in &resp.headers_dict { + if key.to_lowercase() == "host" { + host = value[0].as_str(); + break; + } + } + + info!( + "{} - User-Agent: {} - Host: {}", + resp.request_line, user_agent, host + ); +} diff --git a/src/helper_runner.rs b/src/helper_runner.rs new file mode 100644 index 0000000..813493a --- /dev/null +++ b/src/helper_runner.rs @@ -0,0 +1,99 @@ +use http_body_util::combinators::BoxBody; +use hyper::server::conn::http1; +use hyper::service::service_fn; +use hyper::{Request, Response}; + +use anyhow::Result; +use bytes::Bytes; +use hyper_util::rt::TokioIo; +use tokio::net::{TcpListener, TcpStream}; + +use env_logger::Env; +use log::{error, info}; + +use std::env; +use std::future::Future; + +pub async fn run_tcp_server(name: &str, port: &str, test_helper: fn(TcpStream) -> Fut) +where + Fut: Future + Send + 'static, +{ + init_logging(); + let addr = format!("0.0.0.0:{port}"); + info!("Starting {name} helper in: {addr}"); + + let listener = TcpListener::bind(addr) + .await + .unwrap_or_else(|e| panic!("Couldn't start {name} server: {e}")); + + loop { + let (socket, _) = match listener.accept().await { + Ok(v) => v, + Err(e) => { + error!("Could not accept new msg: {e}"); + continue; + } + }; + + tokio::spawn(async move { + // Process each socket concurrently. + (test_helper)(socket).await + }); + } +} + +pub async fn run_http_server(name: &str, socket: &str, handler: F) +where + F: Fn(Request) -> Fut + Clone + Send + 'static, + Fut: std::future::Future>>> + + Send + + 'static, +{ + init_logging(); + info!("Starting {name} server..."); + + let addr = format!("0.0.0.0:{socket}"); + let listener = TcpListener::bind(addr.clone()) + .await + .unwrap_or_else(|e| panic!("Couldn't start {name} server: {e}")); + + info!("Listening on http://{addr}"); + + loop { + let (stream, _) = match listener.accept().await { + Ok(v) => v, + Err(e) => { + error!("Could not accept new msg: {e}"); + continue; + } + }; + + let io = TokioIo::new(stream); + let handler = handler.clone(); + + tokio::task::spawn(async move { + if let Err(err) = http1::Builder::new() + .preserve_header_case(true) + .serve_connection(io, service_fn(handler)) + .await + { + error!("Error serving connection: {err:?}"); + } + }); + } +} + +pub fn init_logging() { + env_logger::Builder::from_env(Env::default().default_filter_or("info")).init(); +} + +pub fn read_port(default: &str) -> String { + let args: Vec = env::args().collect(); + if let Some(i) = args.iter().position(|s| s == "--port") { + args.get(i + 1) + .expect("Missing argument for --port. Usage: --port ") + .clone() + } else { + default.into() + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..bdab3e8 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1 @@ +pub mod helper_runner; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..8963754 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello Oonitarian!"); +}