subunit-0.3.1/.cargo_vcs_info.json0000644000000001361046102023000125500ustar { "git": { "sha1": "86a0aa0afc643dc70a6eac0f6538d3c1e523603e" }, "path_in_vcs": "" }subunit-0.3.1/.github/dependabot.yml000064400000000000000000000007671046102023000155200ustar 00000000000000# To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for all configuration options: # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates version: 2 updates: - package-ecosystem: "cargo" # See documentation for possible values directory: "/" # Location of package manifests schedule: interval: "daily" subunit-0.3.1/.github/workflows/main.yml000064400000000000000000000014701046102023000163640ustar 00000000000000--- name: CI on: push: branches: [ master ] pull_request: branches: [ master ] env: CARGO_TERM_COLOR: always jobs: build: runs-on: ${{ matrix.os }} strategy: matrix: os: ["ubuntu-latest", "macOS-latest", "windows-latest"] steps: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: toolchain: stable profile: minimal components: rustfmt - uses: actions/setup-python@v2 with: python-version: 3.8 - name: Build run: cargo build --verbose - name: Rust fmt run: cargo fmt -- --check - name: Clippy run: cargo clippy -- -D warnings - name: Doc tests run: cargo test --doc - name: Install test deps run: cargo test --verbose --all subunit-0.3.1/.gitignore000064400000000000000000000000371046102023000133060ustar 00000000000000 /target **/*.rs.bk Cargo.lock subunit-0.3.1/Cargo.lock0000644000000372011046102023000105260ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "android_system_properties" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" dependencies = [ "libc", ] [[package]] name = "async-stream" version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" dependencies = [ "async-stream-impl", "futures-core", "pin-project-lite", ] [[package]] name = "async-stream-impl" version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "async-trait" version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "bitflags" version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "bumpalo" version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "bytes" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cc" version = "1.2.57" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" dependencies = [ "find-msvc-tools", "shlex", ] [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chrono" version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", "js-sys", "num-traits", "wasm-bindgen", "windows-link", ] [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "crc32fast" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] [[package]] name = "darling" version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" dependencies = [ "darling_core", "darling_macro", ] [[package]] name = "darling_core" version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", "syn", ] [[package]] name = "darling_macro" version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ "darling_core", "quote", "syn", ] [[package]] name = "enumset" version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25b07a8dfbbbfc0064c0a6bdf9edcf966de6b1c33ce344bdeca3b41615452634" dependencies = [ "enumset_derive", ] [[package]] name = "enumset_derive" version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f43e744e4ea338060faee68ed933e46e722fb7f3617e722a5772d7e856d8b3ce" dependencies = [ "darling", "proc-macro2", "quote", "syn", ] [[package]] name = "errno" version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", "windows-sys", ] [[package]] name = "find-msvc-tools" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "futures-core" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-sink" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "iana-time-zone" version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "log", "wasm-bindgen", "windows-core", ] [[package]] name = "iana-time-zone-haiku" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" dependencies = [ "cc", ] [[package]] name = "ident_case" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "js-sys" version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ "once_cell", "wasm-bindgen", ] [[package]] name = "libc" version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "lock_api" version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ "scopeguard", ] [[package]] name = "log" version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "memchr" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "mio" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", "wasi", "windows-sys", ] [[package]] name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] name = "once_cell" version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "parking_lot" version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", ] [[package]] name = "parking_lot_core" version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", "windows-link", ] [[package]] name = "pin-project-lite" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "proc-macro2" version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] [[package]] name = "redox_syscall" version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ "bitflags", ] [[package]] name = "rustversion" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ "errno", "libc", ] [[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.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", "windows-sys", ] [[package]] name = "subunit" version = "0.3.1" dependencies = [ "async-stream", "async-trait", "chrono", "crc32fast", "enumset", "thiserror", "tokio", "tokio-stream", "tokio-util", "winnow", ] [[package]] name = "syn" version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "thiserror" version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tokio" version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ "bytes", "libc", "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", "tokio-macros", "windows-sys", ] [[package]] name = "tokio-macros" version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tokio-stream" version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" dependencies = [ "futures-core", "pin-project-lite", "tokio", ] [[package]] name = "tokio-util" version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", "futures-sink", "pin-project-lite", "tokio", ] [[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasm-bindgen" version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ "bumpalo", "proc-macro2", "quote", "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" dependencies = [ "unicode-ident", ] [[package]] name = "windows-core" version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", "windows-link", "windows-result", "windows-strings", ] [[package]] name = "windows-implement" version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "windows-interface" version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-result" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ "windows-link", ] [[package]] name = "windows-strings" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ "windows-link", ] [[package]] name = "windows-sys" version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ "windows-link", ] [[package]] name = "winnow" version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" dependencies = [ "memchr", ] subunit-0.3.1/Cargo.toml0000644000000032611046102023000105500ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" name = "subunit" version = "0.3.1" authors = ["Matthew Treinish "] build = false autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "A subunit v2 protocol implementation in Rust" readme = "README.md" keywords = ["subunit"] categories = ["development-tools::testing"] license = "Apache-2.0" repository = "https://github.com/mtreinish/subunit-rust" [features] async = [ "dep:async-stream", "dep:async-trait", "dep:tokio", "dep:tokio-stream", ] default = [ "async", "sync", ] sync = [] v1 = [ "async", "dep:winnow", ] [lib] name = "subunit" path = "src/lib.rs" [dependencies.async-stream] version = "0.3" optional = true [dependencies.async-trait] version = "0.1.81" optional = true [dependencies.chrono] version = "0.4.38" [dependencies.crc32fast] version = "1.3" [dependencies.enumset] version = "1.1.3" [dependencies.thiserror] version = "2.0.6" [dependencies.tokio] version = "1.0" features = ["full"] optional = true [dependencies.tokio-stream] version = "0.1" optional = true [dependencies.winnow] version = "0.7.0" features = [] optional = true [dev-dependencies.tokio-util] version = "0.7.11" subunit-0.3.1/Cargo.toml.orig000064400000000000000000000016521046102023000142110ustar 00000000000000[package] authors = ["Matthew Treinish "] categories = ["development-tools::testing"] description = "A subunit v2 protocol implementation in Rust" edition = "2021" keywords = ["subunit"] license = "Apache-2.0" name = "subunit" readme = "README.md" repository = "https://github.com/mtreinish/subunit-rust" version = "0.3.1" [features] async = ["dep:async-stream", "dep:async-trait", "dep:tokio", "dep:tokio-stream"] default = ["async", "sync"] sync = [] v1 = ["async", "dep:winnow"] [dependencies] async-stream = { version = "0.3", optional = true } async-trait = { version = "0.1.81", optional = true } chrono = "0.4.38" crc32fast = "1.3" enumset = "1.1.3" thiserror = "2.0.6" tokio = { version = "1.0", optional = true, features = ["full"] } tokio-stream = { version = "0.1", optional = true } winnow = { version = "0.7.0", optional = true, features = [] } [dev-dependencies] tokio-util = { version = "0.7.11" } subunit-0.3.1/LICENSE000064400000000000000000000236371046102023000123360ustar 00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. subunit-0.3.1/README.md000064400000000000000000000102521046102023000125750ustar 00000000000000Subunit Rust ============ [![subunit-rust CI][ci-image]][ci] [![subunit on crates.io][cratesio-image]][cratesio] [ci-image]: https://github.com/mtreinish/subunit-rust/actions/workflows/main.yml/badge.svg [ci]: https://github.com/mtreinish/subunit-rust/actions/workflows/main.yml [cratesio-image]: https://img.shields.io/crates/v/subunit.svg [cratesio]: https://crates.io/crates/subunit This repo contains a implementation of the subunit v2 protocol in Rust. It provides an interface for both writing and reading subunit streams natively in rust. The subunit v2 protocol is documented in the [testing-cabal/subunit](https://github.com/testing-cabal/subunit/blob/master/README.rst#version-2) repository. ## Reading subunit packets Reading subunit packets first requires an object implementing the Read trait containing the subunit stream. The iter_stream() function is used to parse the contents and return an iterator of ScannedItem enums. For example, parsing a subunit stream from a file: ```rust,no_run use std::fs::File; use subunit::io::sync::iter_stream; use subunit::types::stream::ScannedItem; let f = File::open("results.subunit")?; for item in iter_stream(f) { match item? { ScannedItem::Event(event) => { // Process the event println!("Got event: {:?}", event); }, ScannedItem::Bytes(bytes) => { // Process non-event data println!("Got bytes: {:?}", bytes); }, ScannedItem::Unknown(bytes, err) => { // Handle unknown data eprintln!("Unknown data: {:?}", err); }, } } # Ok::<(), Box>(()) ``` In this example, the `results.subunit` file will be opened and parsed with a ScannedItem for each packet in the file. ## Writing subunit packets Writing a subunit packet first requires creating an event structure to describe the contents of the packet. The Event API uses a builder pattern for construction. For example: ```rust use subunit::types::{event::Event, teststatus::TestStatus}; use chrono::{TimeZone, Utc}; let event_start = Event::new(TestStatus::InProgress) .test_id("A_test_id") .datetime(Utc.with_ymd_and_hms(2014, 7, 8, 9, 10, 11).unwrap())? .tag("tag_a") .tag("tag_b") .build(); # Ok::<(), Box>(()) ``` A typical test event normally involves 2 packets though, one to mark the start and the other to mark the finish of a test: ```rust use subunit::types::{event::Event, teststatus::TestStatus}; use chrono::{TimeZone, Utc}; let event_end = Event::new(TestStatus::Success) .test_id("A_test_id") .datetime(Utc.with_ymd_and_hms(2014, 7, 8, 9, 12, 0).unwrap())? .tag("tag_a") .tag("tag_b") .mime_type("text/plain;charset=utf8") .file_content("stdout:''", b"stdout content") .build(); # Ok::<(), Box>(()) ``` Then you'll want to write the packet out to something. Anything that implements the std::io::Write trait can be used for the packets, including things like a File and a TCPStream. In this case we'll use Vec to keep it in memory: ```rust use subunit::serialize::Serializable; use subunit::types::{event::Event, teststatus::TestStatus}; use chrono::{TimeZone, Utc}; let mut subunit_stream: Vec = Vec::new(); let event_start = Event::new(TestStatus::InProgress) .test_id("A_test_id") .datetime(Utc.with_ymd_and_hms(2014, 7, 8, 9, 10, 11).unwrap())? .tag("tag_a") .tag("tag_b") .build(); let event_end = Event::new(TestStatus::Success) .test_id("A_test_id") .datetime(Utc.with_ymd_and_hms(2014, 7, 8, 9, 12, 0).unwrap())? .tag("tag_a") .tag("tag_b") .mime_type("text/plain;charset=utf8") .file_content("stdout:''", b"stdout content") .build(); event_start.serialize(&mut subunit_stream)?; event_end.serialize(&mut subunit_stream)?; # Ok::<(), Box>(()) ``` With this the subunit_stream buffer will contain the contents of the subunit stream for that test event. subunit-0.3.1/src/deserialize.rs000064400000000000000000000077211046102023000147620ustar 00000000000000//! Deserialization of events use crate::{types::number::SubunitNumber, Error, GenResult}; /// Deserialization of Subunit types from a byte slice. pub trait Deserializable { /// The minimum number of bytes that might be required to deserialize this /// type from the front of the slice. If the type cannot be deserialized at /// all, return an error. /// /// The count is a minimum because additional bytes may be required once the /// actual value is available. For instance, the minimum bytes for a UTF8 /// codepoint is 1, buf if the codepoint is a multi-byte codepoint, /// additional bytes are required, and because of the way UTF8 is encoded, /// each byte can only reveal the requirement for one more byte. /// /// However for error handling, knowing how many bytes to skip over is very /// useful, so when required_bytes returns a value <= len(bytes), the caller /// can use that to skip over bytes to the next thing, if deserialising /// fails. fn required_bytes(bytes: &[u8]) -> GenResult; /// Deserialize the type from the slice. fn deserialize(bytes: &[u8]) -> GenResult<(Self, usize)> where Self: Sized; } impl Deserializable for u8 { fn required_bytes(_bytes: &[u8]) -> GenResult { Ok(1) } fn deserialize(bytes: &[u8]) -> GenResult<(u8, usize)> { if bytes.len() < u8::required_bytes(bytes)? { return Err(Error::NotEnoughBytes.into()); } Ok((bytes[0], 1)) } } impl Deserializable for u16 { fn required_bytes(_bytes: &[u8]) -> GenResult { Ok(2) } fn deserialize(bytes: &[u8]) -> GenResult<(u16, usize)> { if bytes.len() < u16::required_bytes(bytes)? { return Err(Error::NotEnoughBytes.into()); } Ok((u16::from_be_bytes(bytes[..2].try_into().unwrap()), 2)) } } impl Deserializable for String { fn required_bytes(bytes: &[u8]) -> GenResult { let required = SubunitNumber::required_bytes(bytes)?; if bytes.len() < required { return Ok(required); } let (length, required) = SubunitNumber::deserialize(&bytes[..required])?; // The length is the number of bytes in the string, plus the length of the number prefixing it Ok(length.as_u32() as usize + required) } fn deserialize(bytes: &[u8]) -> GenResult<(String, usize)> { let (vec, length) = Vec::::deserialize(bytes)?; String::from_utf8(vec) .map(|s| (s, length)) .map_err(|_| Error::InvalidUTF8Sequence.into()) } } impl Deserializable for Vec { fn required_bytes(bytes: &[u8]) -> GenResult { String::required_bytes(bytes) } fn deserialize(bytes: &[u8]) -> GenResult<(Vec, usize)> { let required = String::required_bytes(bytes)?; if bytes.len() < required { return Err(Error::NotEnoughBytes.into()); } let (length, required) = SubunitNumber::deserialize(&bytes[..required])?; // The length is the number of bytes in the string, plus the length of the number prefixing it if bytes.len() < length.as_u32() as usize + required { return Err(Error::NotEnoughBytes.into()); } Ok(( bytes[required..length.as_u32() as usize + required].to_vec(), length.as_u32() as usize + required, )) } } impl Deserializable for Vec { fn required_bytes(_bytes: &[u8]) -> GenResult { unreachable!("Vec::required_bytes is not required for this implementation"); } fn deserialize(bytes: &[u8]) -> GenResult<(Vec, usize)> { let (length, mut offset) = SubunitNumber::deserialize(bytes)?; let mut result = vec![]; for _ in 0..length.as_u32() { let (string, size) = String::deserialize(&bytes[offset..])?; result.push(string); offset += size; } Ok((result, offset)) } } subunit-0.3.1/src/io/async.rs000064400000000000000000000152151046102023000142030ustar 00000000000000//! Asynchronous I/O module use std::collections::VecDeque; use async_stream::try_stream; use tokio::io::AsyncReadExt; use tokio_stream::Stream; use crate::{deserialize::Deserializable, types::stream::ScannedItem, Error, GenError, GenResult}; /// Ask a struct to write itself to some impl AsyncWrite #[async_trait::async_trait] pub trait WriteIntoAsync { /// Write the struct to the writer async fn write_into( &self, writer: &mut (dyn tokio::io::AsyncWrite + Send + Unpin), ) -> std::io::Result<()>; } async fn next( reader: &mut R, buffer: &mut VecDeque, ) -> GenResult> { // VecDequeue doesn't reserve space, and like Read AsyncRead only uses // allocated space (ReadBuf's intent aside). So we use VecDequeue to // minimise overheads, but do not actually read into it. let mut required_bytes = { let buf = buffer.make_contiguous(); match ScannedItem::required_bytes(buf) { Ok(v) => v, Err(e) => Err(GenError::from(e))?, } }; while buffer.len() < required_bytes { let mut read_buffer = [0u8; 8192]; match reader.read(&mut read_buffer).await { Ok(0) => { if buffer.is_empty() { return Ok(None); } // By definition, we have a partial packet or partial byte return Ok(Some(ScannedItem::Unknown( buffer.drain(..).collect(), Error::NotEnoughBytes.into(), ))); } Ok(bytes_read) => { // Might not be enough read yet buffer.extend(read_buffer[..bytes_read].iter()); } Err(e) => Err(GenError::from(e))?, } { let buf = buffer.make_contiguous(); required_bytes = match ScannedItem::required_bytes(buf) { Ok(v) => v, Err(e) => Err(GenError::from(e))?, }; } } // Now we have enough data to do something with it. let buf = buffer.make_contiguous(); match ScannedItem::deserialize(buf) { Ok((ScannedItem::Event(event), used)) => { buffer.drain(..used); Ok(Some(ScannedItem::Event(event))) } Ok((ScannedItem::Bytes(_), _)) => { // Collect all consecutive non-event bytes into a single item let mut bytes = Vec::new(); while let Some(&byte) = buffer.front() { if byte == crate::constants::V2_SIGNATURE { break; } bytes.push(buffer.pop_front().unwrap()); } Ok(Some(ScannedItem::Bytes(bytes))) } Ok((ScannedItem::Unknown(data, e), used)) => { buffer.drain(..used); Ok(Some(ScannedItem::Unknown(data, e))) } Err(e) => { // We know from the loop above that we had enough bytes, and this is not IO: some form of junk. // We have an invalid char or failed crc32 or similar. Ok(Some(ScannedItem::Unknown( buffer.drain(..required_bytes).collect(), e, ))) } } } /// Iterate over a Readable, yielding the contents as `ScannedItems`. pub fn iter_stream( mut reader: R, ) -> impl Stream> { try_stream! { // Maximum buffer needed to process subunit packets is 4MB let mut buffer = VecDeque::::with_capacity(4 * 1024 * 1024); // NB: its likely that an async-native version of the logic would produce a nicer state machine; OTOH this way way have just one implementation of the core. while let Some(item) = next(&mut reader, &mut buffer).await? { yield item; } } } #[cfg(test)] mod tests { use tokio_stream::StreamExt; use crate::{ io::r#async::iter_stream, serialize::Serializable, types::{event::Event, stream::ScannedItem, teststatus::TestStatus}, }; #[tokio::test] async fn test_iter_stream() { // Construct a buffer containing a simple v2 stream let events = vec![ Event::new(TestStatus::Success).test_id("foo").build(), Event::new(TestStatus::Success).test_id("bar").build(), Event::new(TestStatus::Success).test_id("baz").build(), ]; let mut buf = Vec::new(); for event in events { event.serialize(&mut buf).unwrap(); } let stream = iter_stream(&buf[..]); let results = stream .collect::, _>>() .await .unwrap(); assert_eq!(results.len(), 3); } #[tokio::test] async fn test_stream_with_invalid_utf8() { // Test that we can parse a stream with invalid UTF-8 bytes interleaved let event = Event::new(TestStatus::Success).test_id("test").build(); let mut buffer = Vec::new(); // Add some invalid UTF-8 bytes (0xFF is not valid UTF-8 start byte) buffer.extend_from_slice(&[0xFF, 0xFE, 0xFD]); // Add a valid event event.serialize(&mut buffer).unwrap(); // Add more invalid UTF-8 buffer.extend_from_slice(&[0x80, 0x81]); let stream = iter_stream(&buffer[..]); let items: Vec<_> = stream.collect::, _>>().await.unwrap(); // We should get: 1 Bytes item (with 3 bytes), 1 Event, 1 Bytes item (with 2 bytes) assert_eq!(items.len(), 3); match &items[0] { ScannedItem::Bytes(bytes) => assert_eq!(bytes, &[0xFF, 0xFE, 0xFD]), _ => panic!("Expected Bytes, got {:?}", items[0]), } assert!(matches!(items[1], ScannedItem::Event(_))); match &items[2] { ScannedItem::Bytes(bytes) => assert_eq!(bytes, &[0x80, 0x81]), _ => panic!("Expected Bytes, got {:?}", items[2]), } } #[tokio::test] async fn test_no_infinite_loop_on_malformed_stream() { // This test reproduces the infinite loop bug from TODO.infinite-bug.md // Raw subunit v2 packet from a simple test command let data: &[u8] = b"\xb3\x29\x00\x16test1\x20\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xb3"; let stream = iter_stream(data); let items: Vec<_> = stream .take(101) .collect::, _>>() .await .unwrap(); // Should finish in a reasonable number of iterations (likely 1-3) assert!( items.len() <= 10, "Expected few iterations, got {}", items.len() ); } } subunit-0.3.1/src/io/sync.rs000064400000000000000000000236531046102023000140470ustar 00000000000000//! Synchronous I/O module use std::{collections::VecDeque, io::Read}; use crate::{deserialize::Deserializable, types::stream::ScannedItem, Error, GenResult}; /// Ask a struct to write itself to some impl Write pub trait WriteInto { /// Write the struct to the writer fn write_into(&self, writer: &mut dyn std::io::Write) -> std::io::Result<()>; } /// Look for subunit events in an input stream. #[derive(Debug)] pub struct Scanner { buffer: VecDeque, reader: R, read_buf: Box<[u8; 4096]>, } /// Iterate over a Readable, yielding the contents as `ScannedItems`. pub fn iter_stream(reader: R) -> impl Iterator> { // Maximum buffer needed to process subunit packets is 4MB let buffer = VecDeque::::with_capacity(4 * 1024 * 1024); Scanner { buffer, reader, read_buf: Box::new([0u8; 4096]), } } impl Iterator for Scanner where R: Read, { type Item = GenResult; fn next(&mut self) -> Option { loop { let buf = self.buffer.make_contiguous(); let required_bytes = match ScannedItem::required_bytes(buf) { Ok(v) => v, Err(e) => return Some(Err(e)), }; if buf.len() >= required_bytes { // We have enough data - parse it break; } // Need to read more data from the reader // Use the reusable read buffer to avoid allocations match self.reader.read(&mut self.read_buf[..]) { Ok(0) => { // EOF reached - check one more time if we have enough bytes // before declaring this Unknown. This handles the case where // required_bytes returns a conservative estimate that gets // refined as more data becomes available. if self.buffer.is_empty() { return None; } let buf = self.buffer.make_contiguous(); let required_bytes = match ScannedItem::required_bytes(buf) { Ok(v) => v, Err(e) => return Some(Err(e)), }; if buf.len() >= required_bytes { // We actually do have enough data break; } // Truly incomplete packet at EOF return Some(Ok(ScannedItem::Unknown( self.buffer.drain(..).collect(), Error::NotEnoughBytes.into(), ))); } Ok(n) => { // Extend buffer with the bytes we actually read self.buffer.extend(&self.read_buf[..n]); } Err(e) => return Some(Err(e.into())), } } // Now we have enough data to do something with it. let buf = self.buffer.make_contiguous(); match ScannedItem::deserialize(buf) { Ok((ScannedItem::Event(event), used)) => { self.buffer.drain(..used); Some(Ok(ScannedItem::Event(event))) } Ok((ScannedItem::Bytes(_), _)) => { // Collect all consecutive non-event bytes into a single item let mut bytes = Vec::new(); while let Some(&byte) = self.buffer.front() { if byte == crate::constants::V2_SIGNATURE { break; } bytes.push(self.buffer.pop_front().unwrap()); } Some(Ok(ScannedItem::Bytes(bytes))) } Ok((ScannedItem::Unknown(data, e), used)) => { self.buffer.drain(..used); Some(Ok(ScannedItem::Unknown(data, e))) } Err(e) => { // We know from the loop above that we had enough bytes, and this is not IO: some form of junk. // We have an invalid char or failed crc32 or similar. let buf = self.buffer.make_contiguous(); let required_bytes = ScannedItem::required_bytes(buf).unwrap_or(1); Some(Ok(ScannedItem::Unknown( self.buffer.drain(..required_bytes).collect(), e, ))) } } } } #[cfg(test)] mod tests { use std::io::Cursor; use chrono::NaiveDate; use crate::{ io::sync, serialize::Serializable, types::{event::Event, stream::ScannedItem, teststatus::TestStatus}, }; #[test] fn test_write_full_test_event_with_file_content() { let event = Event::new(TestStatus::InProgress) .test_id("A_test_id") .datetime( NaiveDate::from_ymd_opt(2014, 7, 8) .unwrap() .and_hms_opt(9, 10, 11) .unwrap() .and_utc(), ) .unwrap() .tag("tag_a") .tag("tag_b") .mime_type("text/plain;charset=utf8") .file_content("stdout:''", b"stdout content") .build(); let event_a = Event::new(TestStatus::Failed) .test_id("A_test_id") .datetime( NaiveDate::from_ymd_opt(2014, 7, 8) .unwrap() .and_hms_opt(9, 12, 1) .unwrap() .and_utc(), ) .unwrap() .tag("tag_a") .tag("tag_b") .build(); let mut buffer = event.to_vec().unwrap(); event_a.serialize(&mut buffer).unwrap(); let mut count = 0; for (parsed_event, event) in sync::iter_stream(Cursor::new(&buffer)).zip([event, event_a].iter()) { count += 1; let parsed_event = parsed_event.unwrap(); let ScannedItem::Event(parsed_event) = parsed_event else { panic!("Expected event, got {:?}", parsed_event); }; assert_eq!(*event, parsed_event); } assert_eq!(count, 2, "Expected to read 2 events, got {}", count); } #[test] fn test_scanner_reads_owned_cursor() { // This test exposes the bug: Scanner fails when given ownership of the Cursor // (as opposed to borrowing it like the test above) let event = Event::new(TestStatus::Success).test_id("test").build(); let buffer = event.to_vec().unwrap(); // Pass owned Cursor - this should work but doesn't due to Scanner bug let mut count = 0; for item in sync::iter_stream(Cursor::new(buffer)) { let item = item.unwrap(); if matches!(item, ScannedItem::Event(_)) { count += 1; } } assert_eq!(count, 1, "Expected 1 event, got {}", count); } #[test] fn test_stream_with_invalid_utf8() { // Test that we can parse a stream with invalid UTF-8 bytes interleaved let event = Event::new(TestStatus::Success).test_id("test").build(); let mut buffer = Vec::new(); // Add some invalid UTF-8 bytes (0xFF is not valid UTF-8 start byte) buffer.extend_from_slice(&[0xFF, 0xFE, 0xFD]); // Add a valid event event.serialize(&mut buffer).unwrap(); // Add more invalid UTF-8 buffer.extend_from_slice(&[0x80, 0x81]); let items: Vec<_> = sync::iter_stream(Cursor::new(&buffer)) .collect::, _>>() .unwrap(); // We should get: 1 Bytes item (with 3 bytes), 1 Event, 1 Bytes item (with 2 bytes) assert_eq!(items.len(), 3); match &items[0] { ScannedItem::Bytes(bytes) => assert_eq!(bytes, &[0xFF, 0xFE, 0xFD]), _ => panic!("Expected Bytes, got {:?}", items[0]), } assert!(matches!(items[1], ScannedItem::Event(_))); match &items[2] { ScannedItem::Bytes(bytes) => assert_eq!(bytes, &[0x80, 0x81]), _ => panic!("Expected Bytes, got {:?}", items[2]), } } #[test] fn test_many_events() { // Test that we can parse a large number of events without losing any const NUM_EVENTS: usize = 3461; let mut buffer = Vec::new(); for i in 0..NUM_EVENTS { let event = Event::new(TestStatus::Success) .test_id(&format!("test_{}", i)) .build(); event.serialize(&mut buffer).unwrap(); } let mut count = 0; for item in sync::iter_stream(Cursor::new(&buffer)) { match item { Ok(ScannedItem::Event(_)) => count += 1, Ok(ScannedItem::Unknown(data, e)) => { panic!( "Unexpected Unknown item at event {}: {} bytes, error: {:?}", count, data.len(), e ); } Ok(ScannedItem::Bytes(_)) => {} Err(e) => panic!("Error reading event: {:?}", e), } } assert_eq!(count, NUM_EVENTS); } #[test] fn test_no_infinite_loop_on_malformed_stream() { // This test reproduces the infinite loop bug from TODO.infinite-bug.md // Raw subunit v2 packet from a simple test command let data: &[u8] = b"\xb3\x29\x00\x16test1\x20\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xb3"; let mut count = 0; for item in sync::iter_stream(Cursor::new(data)) { count += 1; if count > 100 { panic!("Infinite loop detected after {} iterations!", count); } // Just consume the item let _ = item; } // Should finish in a reasonable number of iterations (likely 1-3) assert!(count <= 10, "Expected few iterations, got {}", count); } } subunit-0.3.1/src/io.rs000064400000000000000000000002541046102023000130630ustar 00000000000000//! Convenience functions for reading and writing subunit packets in different IO models #[cfg(feature = "async")] pub mod r#async; #[cfg(feature = "sync")] pub mod sync; subunit-0.3.1/src/lib.rs000064400000000000000000000063401046102023000132240ustar 00000000000000// Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #![deny(missing_docs)] //! Implementation of the Subunit protocol in Rust. For the protocol definition, //! see the [Subunit Protocol //! Specification](https://github.com/testing-cabal/subunit/blob/main/README.rst). //! This crate contains both a v1 and v2 implementation of the protocol. v1 is //! disabled by default but can be enabled via the `v1` feature. /// Types representing the Subunit protocol pub mod types { pub mod event; pub mod eventfeatures; pub mod file; pub mod number; pub mod stream; pub mod teststatus; pub mod timestamp; } pub mod deserialize; pub mod io; pub mod serialize; /// Constants defined by the Subunit protocol pub mod constants { /// The Subunit v2 protocol signature pub static V2_SIGNATURE: u8 = 0xb3; /// Maximum packet length pub static MAX_PACKET_LENGTH: u32 = 4 * 1024 * 1024; /// Maximum value for a number pub static MAX_NUMBER_VALUE: u32 = 0x3fffffff; /// Mask for a number kind pub static NUMBER_KIND_MASK: u8 = 0xc0; /// Mask for a number value pub static NUMBER_VALUE_MASK: u8 = 0x3f; /// The Subunit v2 protocol version pub static VERSION2: u16 = 0x2000; } #[cfg(feature = "v1")] pub mod v1; use std::fmt::Debug; use thiserror::Error as ThisError; #[derive(ThisError)] // Allow missing docs, because #[error] will generate them #[allow(missing_docs)] /// Error type for parsing and serializing Subunit packets pub enum Error { #[error("Value is too large to encode")] TooLarge, #[error("Invalid packet header: size {} < header size {}", _0, _1)] LengthTooSmall(u32, u32), #[error("Internal logic error {}", _0)] Internal(String), #[error("Invalid UTF8")] InvalidUTF8Sequence, #[error("Not enough bytes")] NotEnoughBytes, #[error("Invalid signature")] InvalidSignature, #[error("Bad version {:#x}", _0)] BadVersion(u16), #[error("CRC32 Mismatch measured: {:#02x} != stored: {:#02x}", _0, _1)] CRC32Mismatch(u32, u32), #[error("Invalid timestamp secs: {} nsecs: {}", _0, _1)] InvalidTimestamp(u32, u32), #[error("IO Error: {}", _0)] IO(#[from] std::io::Error), #[cfg(feature = "v1")] #[error("V1 Parsing error: {:?}", _0)] V1Parse(String), } #[cfg(feature = "v1")] impl From> for Error where I: std::fmt::Debug, O: std::fmt::Debug, { fn from(err: winnow::error::ParseError) -> Self { Error::V1Parse(format!("{:?}", err)) } } impl Debug for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self) } } type GenError = Box; type GenResult = Result; subunit-0.3.1/src/serialize.rs000064400000000000000000000070651046102023000144520ustar 00000000000000//! Serialization of events use std::io::Write; use crc32fast::Hasher; use crate::{types::number::SubunitNumber, GenResult}; /// Trait that describes the serialization requirements for Subunit events. /// /// Of particular note is the 'look ahead' `wire_size` method, which allows avoiding bulk data copying. pub trait Serializable { /// Returns the size of a given implementor in bytes after serialization. /// /// This is used to calculate the size of the serialized event before data /// copying takes place, in order to write the length-prefix for variable-sized /// components. fn wire_size(&self) -> GenResult; /// Write the instance to the given writer. fn serialize(&self, out: &mut W) -> GenResult<()>; } impl Serializable for Option where T: Serializable, { fn wire_size(&self) -> GenResult { match self { Some(inner) => inner.wire_size(), None => SubunitNumber::new(0), } } fn serialize(&self, out: &mut W) -> GenResult<()> { match self { Some(inner) => inner.serialize(out), None => Ok(()), } } } impl Serializable for Vec where T: Serializable, { fn wire_size(&self) -> GenResult { let mut size = SubunitNumber::new(self.len() as u32)?.wire_size()?; for item in self { size = (size + item.wire_size()?)?; } Ok(size) } fn serialize(&self, out: &mut W) -> GenResult<()> { SubunitNumber::try_from(self.len())?.serialize(out)?; for item in self { item.serialize(out)?; } Ok(()) } } impl Serializable for (T, U) where T: Serializable, U: Serializable, { fn wire_size(&self) -> GenResult { let (a, b) = self; a.wire_size()? + b.wire_size()? } fn serialize(&self, out: &mut W) -> GenResult<()> { let (a, b) = self; a.serialize(out)?; b.serialize(out) } } impl Serializable for u8 { fn wire_size(&self) -> GenResult { SubunitNumber::new(1) } fn serialize(&self, out: &mut W) -> GenResult<()> { out.write_all(&[*self])?; Ok(()) } } impl Serializable for u32 { fn wire_size(&self) -> GenResult { SubunitNumber::new(4) } fn serialize(&self, out: &mut W) -> GenResult<()> { out.write_all(&self.to_be_bytes())?; Ok(()) } } impl Serializable for String { fn wire_size(&self) -> GenResult { self.len() + SubunitNumber::new(self.len() as u32)?.wire_size()? } fn serialize(&self, out: &mut W) -> GenResult<()> { SubunitNumber::try_from(self.len())?.serialize(out)?; out.write_all(self.as_bytes())?; Ok(()) } } /// A writer that computes a CRC32 checksum of the data written to it. pub struct Writer<'a, W> { buffer: &'a mut W, hasher: Hasher, } impl<'a, W> Writer<'a, W> where W: Write, { pub(crate) fn new(buffer: &'a mut W) -> Self { Writer { buffer, hasher: Hasher::new(), } } pub(crate) fn finalize(self) -> u32 { self.hasher.finalize() } } impl Write for Writer<'_, W> where W: Write, { fn write(&mut self, buf: &[u8]) -> std::io::Result { self.hasher.update(buf); self.buffer.write(buf) } fn flush(&mut self) -> std::io::Result<()> { self.buffer.flush() } } subunit-0.3.1/src/types/event.rs000064400000000000000000000516271046102023000147530ustar 00000000000000//! Subunit event use std::io::Write; use chrono::{DateTime, Utc}; use crc32fast::Hasher; use enumset::EnumSet; use crate::{ constants::{self, V2_SIGNATURE}, deserialize::Deserializable, serialize::{Serializable, Writer}, Error, GenResult, }; use super::{ eventfeatures::EventFeatures, file::File, number::SubunitNumber, teststatus::TestStatus, timestamp::Timestamp, }; macro_rules! safe_read { ($expr:expr) => { match $expr { ::std::result::Result::Ok(val) => val, ::std::result::Result::Err(size) => { return ::std::result::Result::Ok(size); } } }; } macro_rules! safe_de { ($expr:expr) => { match $expr { ::std::result::Result::Ok(val) => val, ::std::result::Result::Err(_size) => { return ::std::result::Result::Err($crate::Error::NotEnoughBytes.into()); } } }; } /// Construct an event incrementally pub struct EventBuilder(Event); impl EventBuilder { /// Set the test id pub fn test_id(mut self, test_id: &str) -> Self { self.0.test_id = Some(test_id.to_string()); self } /// Set the event timestamp pub fn datetime(mut self, datetime: DateTime) -> GenResult { self.0.timestamp = Some(datetime.try_into()?); Ok(self) } /// Add a tag to the event pub fn tag(mut self, tag: &str) -> Self { if self.0.tags.is_none() { self.0.tags = Some(Vec::new()); } self.0.tags.as_mut().unwrap().push(tag.to_string()); self } /// Set the file mime type pub fn mime_type(mut self, mime_type: &str) -> Self { self.0.file.mime_type = Some(mime_type.to_string()); self } /// Set the file content pub fn file_content(mut self, name: &str, content: &[u8]) -> Self { self.0.file.file = Some((name.to_string(), content.to_vec())); self } /// Set the event as end of file pub fn end_of_file(mut self) -> Self { self.0.file.eof = true; self } /// Set the event as runnable pub fn runnable(mut self) -> Self { self.0.runnable = true; self } /// Set the routing code pub fn route_code(mut self, route_code: &str) -> Self { self.0.route_code = Some(route_code.to_string()); self } /// Build the event pub fn build(self) -> Event { self.0 } } impl EventBuilder {} /// A subunit event /// /// [Docs](https://github.com/testing-cabal/subunit/blob/fc698775674fcbdb9fcc8286d8358c7185647db4/README.rst?plain=1#L147) #[derive(Debug, Clone, PartialEq)] pub struct Event { /// The status of the event pub status: TestStatus, /// The test id if present pub test_id: Option, /// The timestamp if present pub timestamp: Option, /// File content details pub file: File, /// The routing code if present. Routing codes are used to route IO back to test sources pub route_code: Option, /// Event tags if present pub tags: Option>, /// When true indicates that this test (route_code + test_id) is individually runnable pub runnable: bool, } impl Event { /// Construct an event. #[allow(clippy::new_ret_no_self)] pub fn new(status: TestStatus) -> EventBuilder { EventBuilder(Self { status, test_id: None, timestamp: None, file: File::default(), route_code: None, tags: None, runnable: false, }) } /// Write the event to a byte vector. The maximum size of a serialized event /// is 4MiB. /// /// To avoid allocations, a single vector with reserved capacity can be used /// and reused via the `Serializable` trait. /// /// The function can fail if the event is too large to serialize : Subunit /// defines a 4MB limit. On failure, partial content may have been written. pub fn to_vec(&self) -> GenResult> { let size = self.wire_size()?.as_u32() as usize; let mut buffer = Vec::with_capacity(size); self.serialize(&mut buffer)?; Ok(buffer) } fn make_flags(&self) -> u16 { let mut flags = EnumSet::new(); if self.timestamp.is_some() { flags |= EventFeatures::Timestamp; } if self.test_id.is_some() { flags |= EventFeatures::TestId; } if self.tags.is_some() { flags |= EventFeatures::Tags; } if self.file.mime_type.is_some() { flags |= EventFeatures::FileMimeType; } if self.file.file.is_some() { flags |= EventFeatures::FileContent; } if self.file.eof { flags |= EventFeatures::EndOfFile; } if self.route_code.is_some() { flags |= EventFeatures::RoutingCode; } if self.runnable { flags |= EventFeatures::Runnable; } let version = 0x2000_u16; // version 0x2 version | flags.as_repr() | self.status as u16 } fn packet_length(base_length: u32) -> GenResult { // The length of the packet length is self-referential, so we can't // simply serialise the length of all the other components. Instead, we allow extra space for the number of bytes required to encode the length of the packet itself. match base_length { 0..=62 => 1_u32 + base_length, 63..=16381 => 2_u32 + base_length, 16382..=4194300 => 3_u32 + base_length, // == MAX_PACKET_LENGTH _ => return Err(Error::TooLarge.into()), } .try_into() } } impl Serializable for Event { fn wire_size(&self) -> GenResult { // PACKET = SIGNATURE FLAGS PACKET_LENGTH TIMESTAMP? TESTID? TAGS? // MIME? FILECONTENT? ROUTING_CODE? CRC32 let base_length = (V2_SIGNATURE.wire_size()?.as_u32() + 2 // flags u16 // + SubunitNumber(...).wire_size() // packet length- see below + self.timestamp.wire_size()? // timestamp + self.test_id.wire_size()? // test_id + self.tags.wire_size()? // tags + self.file.wire_size()? // file content + self.route_code.wire_size()? // route code + SubunitNumber::new(4_u32)?)?; // crc32 Self::packet_length(base_length.as_u32()) } fn serialize(&self, out: &mut W) -> GenResult<()> { // PACKET = SIGNATURE FLAGS PACKET_LENGTH TIMESTAMP? TESTID? TAGS? // MIME? FILECONTENT? ROUTING_CODE? CRC32 // Hash while writing let mut writer = Writer::new(out); crate::constants::V2_SIGNATURE.serialize(&mut writer)?; // TODO: make a flags struct to make this nicer? let flags = self.make_flags(); writer.write_all(&flags.to_be_bytes())?; let packet_length = self.wire_size()?; packet_length.serialize(&mut writer)?; self.timestamp.serialize(&mut writer)?; self.test_id.serialize(&mut writer)?; self.tags.serialize(&mut writer)?; self.file.serialize(&mut writer)?; self.route_code.serialize(&mut writer)?; // Flush buffer into output and digest to calculate crc32 let checksum = writer.finalize(); out.write_all(&checksum.to_be_bytes())?; Ok(()) } } impl Deserializable for Event { fn required_bytes(bytes: &[u8]) -> GenResult { // PACKET = SIGNATURE FLAGS PACKET_LENGTH TIMESTAMP? TESTID? TAGS? // MIME? FILECONTENT? ROUTING_CODE? CRC32 let mut reader = Reader::new(bytes); let signature = safe_read!(reader.read::()?); if signature != V2_SIGNATURE { return Err(Error::InvalidSignature.into()); } let flags = safe_read!(reader.read::()?); // TODO : wrapper type to avoid duplication? if flags & 0xF000 != constants::VERSION2 { return Err(Error::BadVersion(flags & 0xF00).into()); } let _features = EnumSet::::from_repr_truncated(flags); let packet_length = safe_read!(reader.read::()?); if packet_length.as_u32() > constants::MAX_PACKET_LENGTH { return Err(Error::TooLarge.into()); } if (packet_length.as_u32() as usize) < reader.bytes_read { return Err( Error::LengthTooSmall(packet_length.as_u32(), reader.bytes_read as u32).into(), ); } Ok(packet_length.as_u32() as usize) } fn deserialize(bytes: &[u8]) -> GenResult<(Self, usize)> { // PACKET = SIGNATURE FLAGS PACKET_LENGTH TIMESTAMP? TESTID? TAGS? // MIME? FILECONTENT? ROUTING_CODE? CRC32 let mut reader = Reader::new(bytes); let signature = safe_de!(reader.read::()?); if signature != V2_SIGNATURE { return Err(Error::InvalidSignature.into()); } let flags = safe_de!(reader.read::()?); // TODO : wrapper type to avoid duplication? if flags & 0xF000 != constants::VERSION2 { return Err(Error::BadVersion(flags & 0xF00).into()); } let status = TestStatus::from(flags); let features = EnumSet::::from_repr_truncated(flags); let packet_length = safe_de!(reader.read::()?).as_u32() as usize; if packet_length > constants::MAX_PACKET_LENGTH as usize { return Err(Error::TooLarge.into()); } if packet_length < reader.bytes_read { return Err( Error::LengthTooSmall(packet_length as u32, reader.bytes_read as u32).into(), ); } // Don't permit out of bound reads. From this point on, we don't // pre-check the length of reads. reader.set_slice_end(packet_length)?; let mut result = Event { status, test_id: None, timestamp: None, file: File::default(), route_code: None, tags: None, runnable: false, }; // It is temping to iterate over the features, but the wire order iteration matters - TODO: see if that is possible if features.contains(EventFeatures::Reserved) { return Err(Error::Internal("Reserved feature".to_string()).into()); } if features.contains(EventFeatures::Timestamp) { let timestamp = safe_de!(reader.read_without_estimating::()?); result.timestamp = Some(timestamp); } if features.contains(EventFeatures::TestId) { let test_id = safe_de!(reader.read_without_estimating::()?); result.test_id = Some(test_id); } if features.contains(EventFeatures::Tags) { let tags = safe_de!(reader.read_without_estimating::>()?); result.tags = Some(tags); } if features.contains(EventFeatures::Runnable) { result.runnable = true; } if features.contains(EventFeatures::EndOfFile) { result.file.eof = true } // TODO: make safe_de reusable without hashing and push this down to the file struct if features.contains(EventFeatures::FileMimeType) { let mime = safe_de!(reader.read_without_estimating::()?); result.file.mime_type = Some(mime); } if features.contains(EventFeatures::FileContent) { let name = safe_de!(reader.read_without_estimating::()?); let content = safe_de!(reader.read_without_estimating::>()?); result.file.file = Some((name, content)); } if features.contains(EventFeatures::RoutingCode) { let route_code = safe_de!(reader.read_without_estimating::()?); result.route_code = Some(route_code); } let packet_crc32 = u32::from_be_bytes( reader.bytes[reader.bytes_read..reader.bytes_read + 4] .try_into() .unwrap(), ); let measured_crc32 = reader.finalize(); if measured_crc32 != packet_crc32 { return Err(Error::CRC32Mismatch(measured_crc32, packet_crc32).into()); } Ok((result, packet_length)) } } /// Helper to avoid some boilerplate in deserialization struct Reader<'a> { bytes: &'a [u8], bytes_read: usize, hasher: Hasher, } impl<'a> Reader<'a> { fn new(bytes: &'a [u8]) -> Self { Self { bytes, bytes_read: 0, hasher: Hasher::new(), } } fn read(&mut self) -> GenResult> where T: Deserializable, { let required = T::required_bytes(&self.bytes[self.bytes_read..])?; if required > self.bytes.len() - self.bytes_read { return Ok(Err(required + self.bytes_read)); } let val = T::deserialize(&self.bytes[self.bytes_read..])?; self.hasher .update(&self.bytes[self.bytes_read..self.bytes_read + val.1]); self.bytes_read += val.1; Ok(Ok(val.0)) } /// read(), but trust that the length is correct and bounds checking will happen in deserialize. fn read_without_estimating(&mut self) -> GenResult> where T: Deserializable, { let val = T::deserialize(&self.bytes[self.bytes_read..])?; self.hasher .update(&self.bytes[self.bytes_read..self.bytes_read + val.1]); self.bytes_read += val.1; Ok(Ok(val.0)) } fn finalize(self) -> u32 { self.hasher.finalize() } /// Sets the slice end to a given length: this prevents reading past the end of the length. fn set_slice_end(&mut self, length: usize) -> GenResult<()> { if length > self.bytes.len() { return Err(Error::NotEnoughBytes.into()); } self.bytes = &self.bytes[..length]; Ok(()) } } #[cfg(test)] mod tests { use chrono::{DateTime, NaiveDate}; use crate::{ deserialize::Deserializable, serialize::Serializable, types::{event::Event, teststatus::TestStatus}, }; #[test] fn test_write_event() { let event = Event::new(TestStatus::InProgress) .test_id("A_test_id") .datetime( NaiveDate::from_ymd_opt(2014, 7, 8) .unwrap() .and_hms_opt(9, 10, 11) .unwrap() .and_utc(), ) .unwrap() .tag("tag_a") .tag("tag_b") .build(); let buffer = event.to_vec().unwrap(); let out_event = Event::deserialize(&buffer).unwrap().0; assert_eq!(event, out_event); } #[test] fn test_write_full_test_event_with_file_content() { let event = Event::new(TestStatus::InProgress) .test_id("A_test_id") .datetime( NaiveDate::from_ymd_opt(2014, 7, 8) .unwrap() .and_hms_opt(9, 10, 11) .unwrap() .and_utc(), ) .unwrap() .tag("tag_a") .tag("tag_b") .mime_type("text/plain;charset=utf8") .file_content("stdout:''", b"stdout content") .build(); let event_a = Event::new(TestStatus::Failed) .test_id("A_test_id") .datetime( NaiveDate::from_ymd_opt(2014, 7, 8) .unwrap() .and_hms_opt(9, 12, 1) .unwrap() .and_utc(), ) .unwrap() .tag("tag_a") .tag("tag_b") .build(); let mut buffer = event.to_vec().unwrap(); event_a.serialize(&mut buffer).unwrap(); let mut offset = 0; for event in [event, event_a].iter() { let (parsed_event, length) = Event::deserialize(&buffer[offset..]).unwrap(); assert_eq!(*event, parsed_event); offset += length; } } #[test] fn test_reference_values() { #[track_caller] fn assert_reference(event: Event, buffer: &[u8]) { // We can parse the reference output let (parsed, length) = Event::deserialize(buffer).unwrap(); assert_eq!(length, buffer.len()); assert_eq!(parsed, event); // We can serialize and it matches the reference output let serialized = event.to_vec().unwrap(); assert_eq!(serialized, buffer); } // Constants from the reference implementation let enumerated: &[u8] = b"\xb3)\x01\x0c\x03foo\x08U_\x1b"; assert_reference( Event::new(TestStatus::Enumeration) .test_id("foo") .runnable() .build(), enumerated, ); let inprogress: &[u8] = b"\xb3)\x02\x0c\x03foo\x8e\xc1-\xb5"; assert_reference( Event::new(TestStatus::InProgress) .test_id("foo") .runnable() .build(), inprogress, ); let success: &[u8] = b"\xb3)\x03\x0c\x03fooE\x9d\xfe\x10"; assert_reference( Event::new(TestStatus::Success) .test_id("foo") .runnable() .build(), success, ); let uxsuccess: &[u8] = b"\xb3)\x04\x0c\x03fooX\x98\xce\xa8"; assert_reference( Event::new(TestStatus::UnexpectedSuccess) .test_id("foo") .runnable() .build(), uxsuccess, ); let skip: &[u8] = b"\xb3)\x05\x0c\x03foo\x93\xc4\x1d\r"; assert_reference( Event::new(TestStatus::Skipped) .test_id("foo") .runnable() .build(), skip, ); let fail: &[u8] = b"\xb3)\x06\x0c\x03foo\x15Po\xa3"; assert_reference( Event::new(TestStatus::Failed) .test_id("foo") .runnable() .build(), fail, ); let xfail: &[u8] = b"\xb3)\x07\x0c\x03foo\xde\x0c\xbc\x06"; assert_reference( Event::new(TestStatus::ExpectedFailure) .test_id("foo") .runnable() .build(), xfail, ); let eof: &[u8] = b"\xb3!\x10\x08S\x15\x88\xdc"; assert_reference( Event::new(TestStatus::Undefined) .end_of_file() .runnable() .build(), eof, ); let file_content: &[u8] = b"\xb3!@\x13\x06barney\x03wooA5\xe3\x8c"; assert_reference( Event::new(TestStatus::Undefined) .file_content("barney", b"woo") .runnable() .build(), file_content, ); let mime: &[u8] = b"\xb3! #\x1aapplication/foo; charset=1x3Q\x15"; assert_reference( Event::new(TestStatus::Undefined) .mime_type("application/foo; charset=1") .runnable() .build(), mime, ); let timestamp: &[u8] = b"\xb3+\x03\x13<\x17T\xcf\x80\xaf\xc8\x03barI\x96>-"; assert_reference( Event::new(TestStatus::Success) .test_id("bar") .datetime(DateTime::from_timestamp(1008161999, 45000).unwrap()) .unwrap() .runnable() .build(), timestamp, ); let route_code: &[u8] = b"\xb3-\x03\x13\x03bar\x06source\x9cY9\x19"; assert_reference( Event::new(TestStatus::Success) .test_id("bar") .route_code("source") .runnable() .build(), route_code, ); let runnable: &[u8] = b"\xb3(\x03\x0c\x03foo\xe3\xea\xf5\xa4"; assert_reference( Event::new(TestStatus::Success).test_id("foo").build(), runnable, ); // Tags have no defined order in the protocol. At least today in the Rust implementation, they are ordered as given/observed. let tag1: &[u8] = b"\xb3)\x80\x15\x03bar\x02\x03foo\x03barTHn\xb4"; assert_reference( Event::new(TestStatus::Undefined) .test_id("bar") .tag("foo") .tag("bar") .runnable() .build(), tag1, ); let tag2: &[u8] = b"\xb3)\x80\x15\x03bar\x02\x03bar\x03foo\xf8\xf1\x91o"; assert_reference( Event::new(TestStatus::Undefined) .test_id("bar") .tag("bar") .tag("foo") .runnable() .build(), tag2, ); } #[test] fn packet_length() { assert_eq!(12, Event::packet_length(11).unwrap().as_u32()); } } subunit-0.3.1/src/types/eventfeatures.rs000064400000000000000000000040621046102023000165010ustar 00000000000000//! Event feature and conversion to/from u16. use enumset::EnumSetType; /// Features for a subunit event /// [Docs](https://github.com/testing-cabal/subunit/blob/fc698775674fcbdb9fcc8286d8358c7185647db4/README.rst?plain=1#L262) #[derive(EnumSetType, Debug)] // Note: the discriminants are the bit index (per // `https://docs.rs/enumset/latest/enumset/struct.EnumSet.html#numeric-representation`), // rather than the actual mask value : see // `https://github.com/Lymia/enumset/issues/32` #[enumset(repr = "u16", serialize_repr = "u16")] pub enum EventFeatures { /// Must be zero in version 2. Reserved = 3, // 0x0008, /// EOF marker. EndOfFile = 4, // 0x0010, /// File MIME type is present. FileMimeType = 5, // 0x0020, /// File content is present. FileContent = 6, // 0x0040, /// Tags are present. Tags = 7, // 0x0080, /// Test is 'runnable'. Runnable = 8, // 0x0100, /// Timestamp present. Timestamp = 9, // 0x0200, /// Routing code present. RoutingCode = 10, // 0x0400, /// Test id present. TestId = 11, // 0x0800, } #[cfg(test)] mod test { use super::EventFeatures; use enumset::{enum_set, EnumSet}; #[test] fn bit_representation() { assert_eq!(enum_set! {EventFeatures::Reserved}.as_repr(), 0x0008); assert_eq!(enum_set! {EventFeatures::EndOfFile}.as_repr(), 0x0010); assert_eq!(enum_set! {EventFeatures::FileMimeType}.as_repr(), 0x0020); assert_eq!(enum_set! {EventFeatures::FileContent}.as_repr(), 0x0040); assert_eq!(enum_set! {EventFeatures::Tags}.as_repr(), 0x0080); assert_eq!(enum_set! {EventFeatures::Runnable}.as_repr(), 0x0100); assert_eq!(enum_set! {EventFeatures::Timestamp}.as_repr(), 0x0200); assert_eq!(enum_set! {EventFeatures::RoutingCode}.as_repr(), 0x0400); assert_eq!(enum_set! {EventFeatures::TestId}.as_repr(), 0x0800); // And a representative reverse test assert_eq!( EnumSet::::from_repr(0x0800), enum_set! {EventFeatures::TestId} ); } } subunit-0.3.1/src/types/file.rs000064400000000000000000000027551046102023000145470ustar 00000000000000//! file handling support for subunit V2 use crate::serialize::Serializable; /// A file event in Subunit V2 has a name, optional content, and an optional end of file marker. The files MIME type can /// also be specified. The wire format can represent these all as separate concepts, which leads to a little friction in /// the language bindings. /// /// [Docs](https://github.com/testing-cabal/subunit/blob/fc698775674fcbdb9fcc8286d8358c7185647db4/README.rst?plain=1#L329) #[derive(Debug, Default, Clone, PartialEq)] pub struct File { /// Optional MIME type. pub mime_type: Option, /// Optional File name and content pub file: Option<(String, Vec)>, /// The end of file marker pub eof: bool, } impl Serializable for File { fn wire_size(&self) -> crate::GenResult { self.mime_type.wire_size()? + self.file.wire_size()? // EOF is serialised as a flag } fn serialize(&self, writer: &mut W) -> crate::GenResult<()> { self.mime_type.serialize(writer)?; self.file.serialize(writer)?; Ok(()) } } // TODO: move the file handling here, after Reader without hashing is usable. // impl Deserializable for File { // fn required_bytes(_bytes: &[u8]) -> crate::GenResult { // unimplemented!("File::required_bytes") // } // fn deserialize(bytes: &[u8]) -> crate::GenResult<(File, usize)> { // unimplemented!("File::deserialize") // } // } subunit-0.3.1/src/types/number.rs000064400000000000000000000227271046102023000151210ustar 00000000000000//! Helpers for number types. use std::{fmt::Debug, io::Write, ops::Add}; use crate::{ constants::{self, NUMBER_KIND_MASK, NUMBER_VALUE_MASK}, deserialize::Deserializable, serialize::Serializable, Error, GenError, GenResult, }; /// The concept of the type of the encoding of a number, stored in the 2 most /// significant bits of the number. /// /// [Docs](https://github.com/testing-cabal/subunit/blob/fc698775674fcbdb9fcc8286d8358c7185647db4/README.rst?plain=1#L199) #[derive(Debug, Clone, Copy, PartialEq)] #[repr(u8)] pub(crate) enum NumberType { OneByte = 0b00000000, TwoBytes = 0b01000000, ThreeBytes = 0b10000000, FourBytes = 0b11000000, } impl NumberType { pub fn new(byte: u8) -> NumberType { let number_type = byte & NUMBER_KIND_MASK; match number_type >> 6 { // 0b00, 1 octet 0 => NumberType::OneByte, // 0b01, 2octets 1 => NumberType::TwoBytes, // 0b10, 3 octets 2 => NumberType::ThreeBytes, // 0b11, 4 octets _ => NumberType::FourBytes, } } } /// A subunit wire protocol number /// /// [Docs](https://github.com/testing-cabal/subunit/blob/fc698775674fcbdb9fcc8286d8358c7185647db4/README.rst?plain=1#L199) #[derive(Clone, Copy, PartialEq)] pub enum SubunitNumber { /// A number that fits in one byte OneByte([u8; 1]), /// A number that fits in two bytes, in network order, with encoding mark. TwoBytes([u8; 2]), /// A number that fits in three bytes, in network order, with encoding mark. ThreeBytes([u8; 3]), /// A number that fits in four bytes, in network order, with encoding mark. FourBytes([u8; 4]), } impl Debug for SubunitNumber { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "SubunitNumber({} {:?})", self.as_u32(), self.as_bytes()) } } impl SubunitNumber { /// Creates a new `SubunitNumber` from a u32 value. pub fn new(value: u32) -> GenResult { if value > constants::MAX_NUMBER_VALUE { return Err(Error::TooLarge.into()); } Ok(match value { /* 2^(8-2) */ 0..=63 => SubunitNumber::OneByte([value as u8]), /* 2^(16-2) */ 64..=16383 => { let mut bytes = (value as u16).to_be_bytes(); bytes[0] |= NumberType::TwoBytes as u8; SubunitNumber::TwoBytes(bytes) } /* 2^(24-2) */ 16384..=4194303 => { let mut bytes = value.to_be_bytes(); bytes[1] |= NumberType::ThreeBytes as u8; SubunitNumber::ThreeBytes(bytes[1..].try_into().unwrap()) } /* 2^(32-2) */ _ => { let mut bytes = value.to_be_bytes(); bytes[0] |= NumberType::FourBytes as u8; SubunitNumber::FourBytes(bytes) } }) } /// Returns the number as a u32. pub fn as_u32(&self) -> u32 { match self { SubunitNumber::OneByte(value) => u32::from(value[0]), SubunitNumber::TwoBytes(value) => { u32::from(value[0] & NUMBER_VALUE_MASK) << 8 | u32::from(value[1]) } SubunitNumber::ThreeBytes(value) => { u32::from(value[0] & NUMBER_VALUE_MASK) << 16 | u32::from(value[1]) << 8 | u32::from(value[2]) } SubunitNumber::FourBytes(value) => { u32::from(value[0] & NUMBER_VALUE_MASK) << 24 | u32::from(value[1]) << 16 | u32::from(value[2]) << 8 | u32::from(value[3]) } } } /// Returns the number as a byte slice. pub fn as_bytes(&self) -> &[u8] { match self { SubunitNumber::OneByte(value) => value, SubunitNumber::TwoBytes(value) => value, SubunitNumber::ThreeBytes(value) => value, SubunitNumber::FourBytes(value) => value, } } } impl TryFrom for SubunitNumber { type Error = GenError; fn try_from(value: u32) -> Result { SubunitNumber::new(value) } } impl TryFrom for SubunitNumber { type Error = GenError; fn try_from(value: usize) -> Result { SubunitNumber::new(value.try_into()?) } } impl From for u32 { fn from(value: SubunitNumber) -> Self { value.as_u32() } } impl Add for u32 { type Output = GenResult; fn add(self, other: SubunitNumber) -> Self::Output { let result = self.checked_add(other.as_u32()); match result { Some(value) => Ok(value.try_into()?), None => Err(Error::TooLarge.into()), } } } impl Add for usize { type Output = GenResult; fn add(self, other: SubunitNumber) -> Self::Output { let result = self.checked_add(other.as_u32() as usize); match result { Some(value) => Ok(value.try_into()?), None => Err(Error::TooLarge.into()), } } } impl Add for GenResult { type Output = GenResult; fn add(self, other: SubunitNumber) -> Self::Output { let result = self?.as_u32().checked_add(other.as_u32()); match result { Some(value) => Ok(value.try_into()?), None => Err(Error::TooLarge.into()), } } } impl Add for SubunitNumber { type Output = GenResult; fn add(self, other: SubunitNumber) -> Self::Output { let result = self.as_u32().checked_add(other.as_u32()); match result { Some(value) => Ok(value.try_into()?), None => Err(Error::TooLarge.into()), } } } impl Serializable for SubunitNumber { fn wire_size(&self) -> GenResult { match self.as_u32() { 0..=63 => SubunitNumber::new(1), 64..=16383 => SubunitNumber::new(2), 16384..=4194303 => SubunitNumber::new(3), _ => SubunitNumber::new(4), } } fn serialize(&self, out: &mut W) -> GenResult<()> { out.write_all(self.as_bytes())?; Ok(()) } } impl Deserializable for SubunitNumber { fn required_bytes(bytes: &[u8]) -> GenResult { if bytes.is_empty() { return Ok(1); } Ok(match NumberType::new(bytes[0]) { NumberType::OneByte => 1, NumberType::TwoBytes => 2, NumberType::ThreeBytes => 3, NumberType::FourBytes => 4, }) } fn deserialize(bytes: &[u8]) -> GenResult<(SubunitNumber, usize)> { if bytes.is_empty() { return Err(Error::NotEnoughBytes.into()); } let size = SubunitNumber::required_bytes(bytes)?; if bytes.len() < size { return Err(Error::NotEnoughBytes.into()); } let b = &bytes[..size]; // The unwraps are infallible - the size is matched Ok(( match NumberType::new(bytes[0]) { NumberType::OneByte => SubunitNumber::OneByte(b.try_into().unwrap()), NumberType::TwoBytes => SubunitNumber::TwoBytes(b.try_into().unwrap()), NumberType::ThreeBytes => SubunitNumber::ThreeBytes(b.try_into().unwrap()), NumberType::FourBytes => SubunitNumber::FourBytes(b.try_into().unwrap()), }, size, )) } } #[cfg(test)] mod tests { use super::NumberType; use super::SubunitNumber; #[test] fn test_number_type() { assert_eq!(NumberType::new(0b00000000), NumberType::OneByte); assert_eq!(NumberType::new(0b00111111), NumberType::OneByte); assert_eq!(NumberType::new(0b01000000), NumberType::TwoBytes); assert_eq!(NumberType::new(0b01111111), NumberType::TwoBytes); assert_eq!(NumberType::new(0b10000000), NumberType::ThreeBytes); assert_eq!(NumberType::new(0b10111111), NumberType::ThreeBytes); assert_eq!(NumberType::new(0b11000000), NumberType::FourBytes); assert_eq!(NumberType::new(0b11111111), NumberType::FourBytes); } #[test] fn test_subunit_number() { let number = SubunitNumber::new(0).unwrap(); assert_eq!(number.as_u32(), 0); assert_eq!(number.as_bytes(), &[0]); let number = SubunitNumber::new(63).unwrap(); assert_eq!(number.as_u32(), 63); assert_eq!(number.as_bytes(), &[0b00111111]); let number = SubunitNumber::new(64).unwrap(); assert_eq!(number.as_u32(), 64); assert_eq!(number.as_bytes(), &[0b01000000, 64]); let number = SubunitNumber::new(16383).unwrap(); assert_eq!(number.as_u32(), 16383); assert_eq!(number.as_bytes(), &[0b01111111, 255]); let number = SubunitNumber::new(16384).unwrap(); assert_eq!(number.as_u32(), 16384); assert_eq!(number.as_bytes(), &[0b10000000, 64, 0]); let number = SubunitNumber::new(4194303).unwrap(); assert_eq!(number.as_u32(), 4194303); assert_eq!(number.as_bytes(), &[0b10111111, 255, 255]); let number = SubunitNumber::new(4194304).unwrap(); assert_eq!(number.as_u32(), 4194304); assert_eq!(number.as_bytes(), &[0b11000000, 64, 0, 0]); let number = SubunitNumber::new(0x3FFFFFFF).unwrap(); assert_eq!(number.as_u32(), 0x3FFFFFFF); assert_eq!(number.as_bytes(), &[0b11111111, 255, 255, 255]); } } subunit-0.3.1/src/types/stream.rs000064400000000000000000000040151046102023000151120ustar 00000000000000//! Support for streams of subunit events. use crate::{ constants::V2_SIGNATURE, deserialize::Deserializable, types::event::Event, Error, GenError, GenResult, }; /// Items in a subunit stream #[derive(Debug)] pub enum ScannedItem { /// Non-event data - raw bytes that are not part of a subunit event. /// This data is interleaved with the subunit stream (e.g., stdout/stderr). Bytes(Vec), /// A subunit event Event(Event), /// Bytes that that are neither valid non-event data nor a valid /// Subunit packet. Could be: interrupted bytes of either at the end of a /// stream, striped and corrupted data, or a Subunit packet with a bad checksum Unknown(Vec, GenError), } impl Deserializable for ScannedItem { fn required_bytes(bytes: &[u8]) -> GenResult { // If it's an event, return the event's required bytes // Otherwise, just consume 1 byte at a time for non-event data Event::required_bytes(bytes).or(Ok(1)) } fn deserialize(bytes: &[u8]) -> GenResult<(Self, usize)> { // TODO: likely the unknown case is not handled thoroughly enough: we should fuzz this. if bytes.is_empty() { return Err(Error::NotEnoughBytes.into()); } if bytes[0] == V2_SIGNATURE { match Event::deserialize(bytes) { Ok((event, used)) => Ok((ScannedItem::Event(event), used)), Err(e) => { // In the normal codepath, hitting deserialize implies required_bytes succeeded. let packet_length = Event::required_bytes(bytes)?; // Probably a corrupt packet, but we can't know how much is corrupt. Ok(( ScannedItem::Unknown(bytes[..packet_length].to_vec(), e), packet_length, )) } } } else { // Non-event data - just forward the byte as-is Ok((ScannedItem::Bytes(vec![bytes[0]]), 1)) } } } subunit-0.3.1/src/types/teststatus.rs000064400000000000000000000026361046102023000160510ustar 00000000000000//! Test status enum and conversion to/from u16. /// Status of a test case. /// [Docs](https://github.com/testing-cabal/subunit/blob/fc698775674fcbdb9fcc8286d8358c7185647db4/README.rst?plain=1#L287) /// /// This is not modelled as `Option` because it just lines up a bit /// more nicely - e.g. we can implement `From` for `TestStatus` when we can't /// implement it for `Option`. #[derive(Debug, PartialEq, Eq, Clone, Copy)] #[repr(u16)] pub enum TestStatus { /// The test case status is undefined or the event was received outside of a test case Undefined = 0x0, /// The test was enumerated but not run Enumeration = 0x1, /// The test is in progress InProgress = 0x2, /// The test was successful Success = 0x3, /// The test was successful but was expected to fail UnexpectedSuccess = 0x4, /// The test was skipped Skipped = 0x5, /// The test failed Failed = 0x6, /// The test failed as was expected ExpectedFailure = 0x7, } impl From for TestStatus { fn from(value: u16) -> Self { match value & 0x7 { 0x0 => Self::Undefined, 0x1 => Self::Enumeration, 0x2 => Self::InProgress, 0x3 => Self::Success, 0x4 => Self::UnexpectedSuccess, 0x5 => Self::Skipped, 0x6 => Self::Failed, _ /* 0x7 */ => Self::ExpectedFailure, } } } subunit-0.3.1/src/types/timestamp.rs000064400000000000000000000137671046102023000156400ustar 00000000000000//! Subunit timestamps use std::io::Write; use chrono::{DateTime, Utc}; use crate::{ deserialize::Deserializable, serialize::Serializable, types::number::SubunitNumber, Error, GenError, GenResult, }; /// Subunit timestamps are UTC time since epoch with a u32 seconds component and /// a SubunitNumber nanoseconds component. /// /// [Docs](https://github.com/testing-cabal/subunit/blob/fc698775674fcbdb9fcc8286d8358c7185647db4/README.rst?plain=1#L315) #[derive(Debug, Clone, PartialEq)] pub struct Timestamp { /// The seconds component of the timestamp pub seconds: u32, /// The nanoseconds component of the timestamp pub nanoseconds: SubunitNumber, } impl Serializable for Timestamp { fn wire_size(&self) -> GenResult { // TIMESTAMP = SECONDS NANOS self.seconds.wire_size()? + self.nanoseconds.wire_size()? } fn serialize(&self, out: &mut W) -> GenResult<()> { self.seconds.serialize(out)?; self.nanoseconds.serialize(out) } } impl Deserializable for Timestamp { fn required_bytes(bytes: &[u8]) -> GenResult { // TIMESTAMP = SECONDS NANOS let required = 5; if bytes.len() < required { return Ok(required); } let required = SubunitNumber::required_bytes(&bytes[4..5])?; // The length is the 4 for the u32 in seconds + the variable length nanos Ok(4 + required) } fn deserialize(bytes: &[u8]) -> GenResult<(Timestamp, usize)> { let required = Timestamp::required_bytes(bytes)?; if bytes.len() < required { return Err(Error::NotEnoughBytes.into()); } // infallible via guard above let seconds = u32::from_be_bytes(bytes[..4].try_into().unwrap()); let nanoseconds = SubunitNumber::deserialize(&bytes[4..required])?.0; Ok(( Timestamp { seconds, nanoseconds, }, required, )) } } impl TryFrom> for Timestamp { type Error = GenError; fn try_from(dt: DateTime) -> GenResult { let timestamp_i64 = dt.timestamp(); // Validate that the timestamp fits in u32 (0 to 4294967295) // This covers dates from 1970-01-01 00:00:00 UTC to 2106-02-07 06:28:15 UTC let seconds = u32::try_from(timestamp_i64) .map_err(|_| Error::InvalidTimestamp(timestamp_i64 as u32, 0))?; let nanoseconds = dt.timestamp_subsec_nanos().try_into()?; Ok(Self { seconds, nanoseconds, }) } } impl TryFrom for DateTime { type Error = GenError; fn try_from(ts: Timestamp) -> GenResult { DateTime::from_timestamp(ts.seconds as i64, ts.nanoseconds.into()) .ok_or_else(|| Error::InvalidTimestamp(ts.seconds, ts.nanoseconds.into()).into()) } } #[cfg(test)] mod tests { use super::*; use chrono::TimeZone; #[test] fn test_timestamp_valid_epoch() { // Test epoch (1970-01-01 00:00:00 UTC) let dt = Utc.timestamp_opt(0, 0).unwrap(); let ts = Timestamp::try_from(dt).unwrap(); assert_eq!(ts.seconds, 0); assert_eq!(u32::from(ts.nanoseconds), 0); // Convert back let dt_back: DateTime = ts.try_into().unwrap(); assert_eq!(dt, dt_back); } #[test] fn test_timestamp_valid_recent() { // Test a recent timestamp (2024-01-01 00:00:00 UTC) let dt = Utc.timestamp_opt(1704067200, 123456789).unwrap(); let ts = Timestamp::try_from(dt).unwrap(); assert_eq!(ts.seconds, 1704067200); assert_eq!(u32::from(ts.nanoseconds), 123456789); // Convert back let dt_back: DateTime = ts.try_into().unwrap(); assert_eq!(dt, dt_back); } #[test] fn test_timestamp_max_u32() { // Test maximum u32 value (2106-02-07 06:28:15 UTC) let dt = Utc.timestamp_opt(u32::MAX as i64, 0).unwrap(); let ts = Timestamp::try_from(dt).unwrap(); assert_eq!(ts.seconds, u32::MAX); assert_eq!(u32::from(ts.nanoseconds), 0); // Convert back let dt_back: DateTime = ts.try_into().unwrap(); assert_eq!(dt, dt_back); } #[test] fn test_timestamp_overflow_beyond_u32() { // Test timestamp beyond u32::MAX (year 2107) let dt = Utc.timestamp_opt(u32::MAX as i64 + 1, 0).unwrap(); let result = Timestamp::try_from(dt); assert!(result.is_err()); } #[test] fn test_timestamp_negative() { // Test negative timestamp (before epoch) let dt = Utc.timestamp_opt(-1, 0).unwrap(); let result = Timestamp::try_from(dt); assert!(result.is_err()); } #[test] fn test_timestamp_invalid_nanoseconds() { // Test invalid nanoseconds (> 1 billion) // Note: SubunitNumber has a max value constraint, so we use 999999999 which is valid // for SubunitNumber but we can create a test with chrono that would fail // The main validation happens in DateTime::from_timestamp let ts = Timestamp { seconds: u32::MAX, nanoseconds: SubunitNumber::try_from(999_999_999u32).unwrap(), }; // This should succeed as it's a valid timestamp let result: Result, _> = ts.try_into(); assert!(result.is_ok()); } #[test] fn test_timestamp_roundtrip() { // Test various timestamps roundtrip correctly let test_cases = vec![ (0, 0), (1, 1), (1000000000, 500000000), (1704067200, 999999999), (u32::MAX, 0), ]; for (secs, nanos) in test_cases { let dt = Utc.timestamp_opt(secs as i64, nanos).unwrap(); let ts = Timestamp::try_from(dt).unwrap(); assert_eq!(ts.seconds, secs); assert_eq!(u32::from(ts.nanoseconds), nanos); let dt_back: DateTime = ts.try_into().unwrap(); assert_eq!(dt, dt_back); } } } subunit-0.3.1/src/v1.rs000064400000000000000000001776371046102023000130260ustar 00000000000000//! V1 protocol implementation. //! //! This module provides both synchronous and asynchronous parsers for the //! Subunit v1 protocol. Use `parse_sync()` for synchronous parsing and //! `parse()` for asynchronous parsing. The primary use of v1 is scaffolding //! to support upgrading to v2. use core::fmt; use std::{fmt::Debug, mem}; use async_stream::stream; use chrono::{DateTime, Utc}; use tokio::io::{AsyncBufRead, AsyncBufReadExt as _, AsyncWriteExt}; use tokio_stream::Stream; use winnow::BStr; #[cfg(feature = "sync")] use crate::io::sync::WriteInto; use crate::{io::r#async::WriteIntoAsync, Error}; /// The default content details for simple bracketed details pub static TRACEBACK_NAME: &str = "traceback"; /// The default MIME type for traceback content pub static X_TRACEBACK: &str = "text/x-traceback;charset=utf-8"; /// An event from a Subunit v1 stream #[derive(Clone, Debug, PartialEq)] pub enum Event { /// A test started / enumerated TestStart(String), /// A test succeeded TestSuccess(String, Vec), /// A test failed (e.g. assertion failure) TestFailure(String, Vec), /// A test errored (e.g. panic, oom etc) TestError(String, Vec), /// A test was not actually run TestSkip(String, Vec), /// A test failed as expected TestExpectedFailure(String, Vec), /// A test expected to fail succeeded TestUnexpectedSuccess(String, Vec), /// Push the progress state ProgressPush, /// Pop the progress state ProgressPop, /// Set the number of expected tests ProgressSet(usize), /// Adjust the number of expected tests up or down. /// /// This is used when a filter discards or adds some tests, preserving the /// expectation that the number of TestStart's observed will match the /// number of expected tests. ProgressCurrent(isize), /// Content that could not be parsed, but was valid utf8 Text(String), /// Binary unparsable content Bytes(Vec), /// Tags command: added tags in .0 and removed in .1 Tags(Vec, Vec), /// What is the time that the next event 'happens' at. Time(DateTime), /// Marks the end of the stream EndOfStream, } impl Event { /// Create a Text or Bytes from a buffer pub fn from_buffer(buf: &[u8]) -> Self { let buf = buf.to_vec(); String::from_utf8(buf) .map(Event::Text) .unwrap_or_else(|e| Event::Bytes(e.into_bytes())) } #[cfg(feature = "sync")] fn write_parts(writer: &mut dyn std::io::Write, parts: &Vec) -> std::io::Result<()> { if parts.is_empty() { writer.write_all(b"\n") } else { writeln!(writer, " [ multipart")?; for part in parts { ::write_into(part, writer)?; } writer.write_all(b"]\n") } } async fn write_parts_async( writer: &mut (dyn tokio::io::AsyncWrite + Send + Unpin), cmd: &str, name: &str, parts: &Vec, ) -> std::io::Result<()> { writer.write_all(cmd.as_bytes()).await?; writer.write_all(name.as_bytes()).await?; if parts.is_empty() { writer.write_all(b"\n").await } else { writer.write_all(b" [ multipart\n").await?; for part in parts { ::write_into(part, writer).await?; } writer.write_all(b"]\n").await } } } #[cfg(feature = "sync")] struct TagsIter<'s, I>(std::iter::Peekable) where I: Iterator + Send; #[cfg(feature = "sync")] impl<'s, I> Iterator for TagsIter<'s, I> where I: Iterator + Send, { type Item = (bool, I::Item); fn next(&mut self) -> Option { self.0.next().map(|e| (self.0.peek().is_none(), e)) } } #[cfg(feature = "sync")] impl WriteInto for Event { fn write_into(&self, writer: &mut dyn std::io::Write) -> std::io::Result<()> { match self { Event::TestStart(name) => writeln!(writer, "test: {name}"), Event::TestSuccess(name, parts) => { write!(writer, "success: {name}")?; Event::write_parts(writer, parts) } Event::TestFailure(name, parts) => { write!(writer, "failure: {name}")?; Event::write_parts(writer, parts) } Event::TestError(name, parts) => { write!(writer, "error: {name}")?; Event::write_parts(writer, parts) } Event::TestSkip(name, parts) => { write!(writer, "skip: {name}")?; Event::write_parts(writer, parts) } Event::TestExpectedFailure(name, parts) => { write!(writer, "xfail: {name}")?; Event::write_parts(writer, parts) } Event::TestUnexpectedSuccess(name, parts) => { write!(writer, "uxsuccess: {name}")?; Event::write_parts(writer, parts) } Event::ProgressPush => writeln!(writer, "progress: push"), Event::ProgressPop => writeln!(writer, "progress: pop"), Event::ProgressSet(v) => writeln!(writer, "progress: {v}"), Event::ProgressCurrent(v) => { if *v >= 0 { writeln!(writer, "progress: +{v}") } else { writeln!(writer, "progress: {v}") } } Event::Text(t) => writeln!(writer, "{t}"), Event::Bytes(b) => writer.write_all(b), Event::Tags(added, removed) => { write!(writer, "tags: ")?; let iter = TagsIter( added .iter() .map(|t| ("", t.as_str())) .chain(removed.iter().map(|t| ("-", t.as_str()))) .peekable(), ); for (last, (prefix, tag)) in iter { write!(writer, "{prefix}{tag}")?; if !last { write!(writer, " ")?; } } writeln!(writer) } Event::Time(t) => { writeln!( writer, "time: {}", t.naive_utc().format("%Y-%m-%d %H:%M:%SZ") ) } Event::EndOfStream => Ok(()), } } } #[async_trait::async_trait] impl WriteIntoAsync for Event { async fn write_into( &self, writer: &mut (dyn tokio::io::AsyncWrite + Send + Unpin), ) -> std::io::Result<()> { match self { Event::TestStart(name) => { writer.write_all("test: ".as_bytes()).await?; writer.write_all(name.as_bytes()).await?; writer.write_all(b"\n").await } Event::TestSuccess(name, parts) => { Event::write_parts_async(writer, "success: ", name, parts).await } Event::TestFailure(name, parts) => { Event::write_parts_async(writer, "failure: ", name, parts).await } Event::TestError(name, parts) => { Event::write_parts_async(writer, "error: ", name, parts).await } Event::TestSkip(name, parts) => { Event::write_parts_async(writer, "skip: ", name, parts).await } Event::TestExpectedFailure(name, parts) => { Event::write_parts_async(writer, "xfail: ", name, parts).await } Event::TestUnexpectedSuccess(name, parts) => { Event::write_parts_async(writer, "uxsuccess: ", name, parts).await } Event::ProgressPush => writer.write_all("progress: push\n".as_bytes()).await, Event::ProgressPop => writer.write_all("progress: pop\n".as_bytes()).await, Event::ProgressSet(v) => { writer.write_all("progress: ".as_bytes()).await?; writer.write_all(v.to_string().as_bytes()).await?; writer.write_all(b"\n").await } Event::ProgressCurrent(v) => { if *v >= 0 { writer.write_all("progress: +".as_bytes()).await?; writer.write_all(v.to_string().as_bytes()).await?; writer.write_all(b"\n").await } else { writer.write_all("progress: ".as_bytes()).await?; writer.write_all(v.to_string().as_bytes()).await?; writer.write_all(b"\n").await } } Event::Text(t) => { writer.write_all(t.as_bytes()).await?; writer.write_all(b"\n").await } Event::Bytes(b) => writer.write_all(b).await, Event::Tags(added, removed) => { writer.write_all("tags: ".as_bytes()).await?; let last = added.len() + removed.len() - 1; let mut pos = 0; for tag in added { writer.write_all(tag.as_bytes()).await?; if last != pos { writer.write_all(b" ").await?; } pos += 1; } for tag in removed { writer.write_all(b"-").await?; writer.write_all(tag.as_bytes()).await?; if last != pos { writer.write_all(b" ").await?; } pos += 1; } writer.write_all(b"\n").await } Event::Time(t) => { writer.write_all("time: ".as_bytes()).await?; writer .write_all(t.format("%Y-%m-%d %H:%M:%SZ").to_string().as_bytes()) .await?; writer.write_all(b"\n").await } Event::EndOfStream => Ok(()), } } } /// A single chunk of a test status file #[derive(Clone, PartialEq)] pub struct Part { /// The content type of the part pub content_type: String, /// The name of the part pub name: String, /// The content of the part pub bytes: Vec, } impl Part { /// Create a new Part pub fn new(content_type: &str, name: &str, bytes: &[u8]) -> Self { Self { content_type: content_type.to_string(), name: name.to_string(), bytes: bytes.to_vec(), } } } impl Debug for Part { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Part") .field("content_type", &self.content_type) .field("name", &self.name) .field("bytes", &BStr::new(&self.bytes)) .finish() } } #[cfg(feature = "sync")] impl WriteInto for Part { fn write_into(&self, writer: &mut dyn std::io::Write) -> std::io::Result<()> { writeln!(writer, "Content-Type: {}", self.content_type)?; writeln!(writer, "{}", self.name)?; if !self.bytes.is_empty() { write!(writer, "{}\r\n", self.bytes.len())?; writer.write_all(&self.bytes)?; } write!(writer, "0\r\n") } } #[async_trait::async_trait] impl WriteIntoAsync for Part { async fn write_into( &self, writer: &mut (dyn tokio::io::AsyncWrite + Send + Unpin), ) -> std::io::Result<()> { writer.write_all("Content-Type: ".as_bytes()).await?; writer.write_all(self.content_type.as_bytes()).await?; writer.write_all("\n".as_bytes()).await?; writer.write_all(self.name.as_bytes()).await?; writer.write_all("\n".as_bytes()).await?; if !self.bytes.is_empty() { writer .write_all(self.bytes.len().to_string().as_bytes()) .await?; writer.write_all("\r\n".as_bytes()).await?; writer.write_all(&self.bytes).await?; } writer.write_all("0\r\n".as_bytes()).await } } /// An event associated with a test #[derive(Clone, Debug, PartialEq)] pub struct TestEvent; /// An event not associated with a test #[derive(Clone, Debug, PartialEq)] pub struct GlobalEvent; /// Construct a parser on an AsyncReadBuf + Debug /// /// Differences from the Python version: /// /// - rather than generating various TestProtocol calls, this iterates over Events /// - bracket escaping is not implemented /// - all the attachments for one test are buffered in memory /// - progress events are not processes within brackets pub fn parse( reader: &mut dyn SubunitStream, ) -> impl Stream> + '_ { TestProtocolServer { reader, state: ParseState::Global, } .stream() } /// Defines the traits needed for a parser stream. pub trait SubunitStream: AsyncBufRead + Unpin + fmt::Debug {} impl SubunitStream for T where T: AsyncBufRead + Unpin + fmt::Debug {} #[cfg(feature = "sync")] /// Defines the traits needed for a synchronous parser stream. pub trait BufReadStream: std::io::BufRead + fmt::Debug {} #[cfg(feature = "sync")] impl BufReadStream for T where T: std::io::BufRead + fmt::Debug {} mod parser { //! Wire -> tokens parser for the subunit v1 protocol. use std::str::from_utf8; use chrono::NaiveDateTime; use winnow::{ ascii::line_ending, combinator::{alt, cut_err, fail, preceded, repeat_till, terminated, trace}, error::{ContextError, ErrMode, FromExternalError, StrContext, StrContextValue}, token::{take, take_till, take_until, take_while}, BStr, Parser, Partial, Stateful, }; type PResult = Result>; use super::{Event, Part, TRACEBACK_NAME, X_TRACEBACK}; #[derive(Debug)] pub(crate) struct State<'s>(pub &'s Option<&'s str>); pub(crate) type Stream<'s> = Stateful, State<'s>>; pub fn parse_cmd_start<'s>(input: &mut Stream<'s>) -> PResult<&'s [u8]> { alt(("test ", "test: ", "testing ", "testing: ")).parse_next(input) } fn parse_cmd_success<'s>(input: &mut Stream<'s>) -> PResult<&'s [u8]> { alt(("success ", "success: ")).parse_next(input) } fn parse_cmd_failure<'s>(input: &mut Stream<'s>) -> PResult<&'s [u8]> { alt(("failure ", "failure: ")).parse_next(input) } fn parse_cmd_error<'s>(input: &mut Stream<'s>) -> PResult<&'s [u8]> { alt(("error ", "error: ")).parse_next(input) } fn parse_cmd_skip<'s>(input: &mut Stream<'s>) -> PResult<&'s [u8]> { alt(("skip ", "skip: ")).parse_next(input) } fn parse_cmd_xfail<'s>(input: &mut Stream<'s>) -> PResult<&'s [u8]> { alt(("xfail ", "xfail: ")).parse_next(input) } fn parse_cmd_uxsuccess<'s>(input: &mut Stream<'s>) -> PResult<&'s [u8]> { alt(("uxsuccess ", "uxsuccess: ")).parse_next(input) } enum DetailStyle { Bracketed, MultiPart, } fn parse_label_and_detail_style<'s>( input: &mut Stream<'s>, line: &'s [u8], ) -> PResult<(&'s str, Option)> { let line = from_utf8(line).map_err(|e| ErrMode::from_external_error(input, e))?; let (name, detail_style) = if let Some(line) = line.strip_suffix(" [") { // Safe from the ends_with check (line, Some(DetailStyle::Bracketed)) } else if let Some(line) = line.strip_suffix(" [ multipart") { // Safe from the ends_with check (line, Some(DetailStyle::MultiPart)) } else { (line, None) }; if name.is_empty() { return Err(ErrMode::Cut(ContextError::new())); } Ok((name, detail_style)) } /// Parse a test label and optional details into basic types fn parse_label_details<'s>(input: &mut Stream<'s>) -> PResult<(String, Vec)> { let line = trace( "label", terminated(take_till(1.., |c| c == b'\r' || c == b'\n'), line_ending), ) .parse_next(input)?; let (label, detail_style) = parse_label_and_detail_style(input, line)?; check_name_match(input, label)?; match detail_style { Some(DetailStyle::Bracketed) => { let label_str = label.to_string(); let parsed = alt(( (&b"]"[..], line_ending).value(&[][..]), ( (take_until(0.., &b"\n]\n"[..]), line_ending).take(), b"]", line_ending, ) .map(|(y, _, _): (&[u8], _, _)| y), )) .context(StrContext::Label("test label")) .context(StrContext::Expected(StrContextValue::Description( "utf8 string", ))) .parse_next(input)?; Ok(( label_str, vec![Part::new(X_TRACEBACK, TRACEBACK_NAME, parsed)], )) } Some(DetailStyle::MultiPart) => repeat_till(0.., parse_part, &b"]\n"[..]) .map(|(parts, _acc): (Vec, _)| (label.to_string(), parts)) .parse_next(input), None => Ok((label.to_string(), vec![])), } } fn check_name_match<'s>( input: &mut Stateful, State<'s>>, name: &'_ str, ) -> PResult<()> { if &Some(name) == input.state.0 { Ok(()) } else { Err(ErrMode::Cut(ContextError::new())) } } /// parse a single length prefixed chunk of a multi-part part fn parse_chunk<'s>(input: &mut Stream<'s>) -> PResult<&'s [u8]> { let len = terminated(take_until(0.., &b"\r\n"[..]), &b"\r\n"[..]) .parse_to::() .parse_next(input)?; let bytes = take(len).parse_next(input)?; Ok(bytes) } /// parse all the bytes of a multi-part part. /// We could consider emitting each length-prefixed chunk to reduce memory pressure, if fn parse_part<'s>(input: &mut Stream<'s>) -> PResult { let (content_type, name) = ( (b"Content-Type: ", take_until(0.., &b"\n"[..]), line_ending) .map(|(_, content_type, _)| content_type), terminated(take_until(0.., &b"\n"[..]), &b"\n"[..]), ) .try_map(|(content_type, name)| { from_utf8(content_type) .and_then(|content_type| from_utf8(name).map(|name| (content_type, name))) }) .parse_next(input)?; let bytes: Vec<&[u8]> = repeat_till(0.., parse_chunk, &b"0\r\n"[..]) .map(|(acc, _term)| acc) .parse_next(input)?; let bytes = bytes.join(&[][..]); Ok(super::Part { content_type: content_type.to_string(), name: name.to_string(), bytes, }) } /// Parse a tags command. fn parse_tags<'s>(input: &mut Stream<'s>) -> PResult { let tags = preceded( "tags: ", terminated(take_till(0.., |c| b"\r\n".contains(&c)), line_ending), ) .try_map(|tags| from_utf8(tags)) .parse_next(input)?; let mut added = vec![]; let mut removed = vec![]; for tag in tags.split_whitespace() { if let Some(stripped) = tag.strip_prefix('-') { removed.push(stripped.to_string()); } else { added.push(tag.to_string()); } } Ok(Event::Tags(added, removed)) } /// Parse a time command. /// time: YYYY-MM-DD HH:MM:SSZ fn parse_time<'s>(input: &mut Stream<'s>) -> PResult { preceded( "time: ", terminated(take_till(0.., |c| b"\r\n".contains(&c)), line_ending), ) .try_map(|time| from_utf8(time)) // Strictly this is too broad, but can't see a way to emulate Z support on chrono otherwise. .try_map(|time| NaiveDateTime::parse_from_str(time, "%Y-%m-%d %H:%M:%SZ")) .map(|time| Event::Time(time.and_utc())) .parse_next(input) } /// Main entry point for parsing when in a test context pub fn parse_subunit_event_in_test<'s>(input: &mut Stream<'s>) -> PResult { // a closure-factory to make this less repetitive might be nice. alt(( preceded(parse_cmd_success, cut_err(parse_label_details)) .map(|(name, details)| Event::TestSuccess(name, details)), preceded(parse_cmd_failure, cut_err(parse_label_details)) .map(|(name, details)| Event::TestFailure(name, details)), preceded(parse_cmd_error, cut_err(parse_label_details)) .map(|(name, details)| Event::TestError(name, details)), preceded(parse_cmd_skip, cut_err(parse_label_details)) .map(|(name, details)| Event::TestSkip(name, details)), preceded(parse_cmd_xfail, cut_err(parse_label_details)) .map(|(name, details)| Event::TestExpectedFailure(name, details)), preceded(parse_cmd_uxsuccess, cut_err(parse_label_details)) .map(|(name, details)| Event::TestUnexpectedSuccess(name, details)), parse_tags, parse_time, fail.context(StrContext::Label("command")) .context(StrContext::Expected(StrContextValue::Description( "test, success, failure, error, skip, or xfail, followed by optional details", ))), )) .parse_next(input) } fn parse_test<'s>(input: &mut Stream<'s>) -> PResult { terminated( ( parse_cmd_start, cut_err(take_till(0.., |c| c == b'\r' || c == b'\n')) .context(StrContext::Label("test label")) .context(StrContext::Expected(StrContextValue::Description( "utf8 string", ))), ), line_ending, ) .try_map(|(_x, y)| from_utf8(y).map(|s| Event::TestStart(s.to_string()))) .parse_next(input) } fn parse_usize<'s>(input: &mut Stream<'s>) -> PResult { take_while(1.., ((b'0'..=b'9'),)) .parse_to() .parse_next(input) } fn parse_isize<'s>(input: &mut Stream<'s>) -> PResult { take_while(1.., ((b'0'..=b'9'),)) .parse_to() .parse_next(input) } /// Main entry point for parsing when in a global context pub fn parse_subunit_event_global<'s>(input: &mut Stream<'s>) -> PResult { alt(( parse_test, terminated(&b"progress: push"[..], line_ending).value(Event::ProgressPush), terminated(&b"progress: pop"[..], line_ending).value(Event::ProgressPop), terminated(preceded(&b"progress: +"[..], parse_isize), line_ending) .map(Event::ProgressCurrent), terminated(preceded(&b"progress: -"[..], parse_isize), line_ending) .map(|v| Event::ProgressCurrent(-v)), terminated(preceded(&b"progress: "[..], parse_usize), line_ending) .map(Event::ProgressSet), terminated(&b"progress: push"[..], line_ending).value(Event::ProgressPush), parse_tags, parse_time, fail.context(StrContext::Label("command")) .context(StrContext::Expected(StrContextValue::StringLiteral( "test ", ))), )) .parse_next(input) } #[cfg(test)] mod tests { use winnow::{ error::{ErrMode, Needed}, BStr, Partial, }; use crate::v1::{Part, TRACEBACK_NAME, X_TRACEBACK}; #[test] fn test_parse_test_start() { let mut input = BStr::new(&b"test "[..]); let state = None; let mut input = super::Stream { input: Partial::new(&mut input), state: super::State(&state), }; let output = super::parse_cmd_start(&mut input); assert_eq!(Ok(&b"test "[..]), output); } #[test] fn test_parse_test_success() { let mut input = BStr::new(&b"success "[..]); let mut state = None; let mut input = super::Stream { input: Partial::new(&mut input), state: super::State(&mut state), }; let output = super::parse_cmd_success(&mut input).unwrap(); assert_eq!(&b"success "[..], output); } #[test] fn test_label_details_partial() { let mut input = BStr::new(&b"label"[..]); let mut state = None; let mut input = super::Stream { input: Partial::new(&mut input), state: super::State(&mut state), }; let output = super::parse_label_details(&mut input).unwrap_err(); assert_eq!(ErrMode::Incomplete(Needed::new(1)), output); } #[test] fn test_label_details_simple() { let mut input = BStr::new(&b"label\n"[..]); let mut state = Some("label"); let mut input = super::Stream { input: Partial::new(&mut input), state: super::State(&mut state), }; let output = super::parse_label_details(&mut input).unwrap(); assert_eq!(("label".into(), vec![]), output); } fn stream<'s>(input: &'s [u8], state: &'s mut Option<&'s str>) -> super::Stream<'s> { let mut input = BStr::new(input); super::Stream { input: Partial::new(&mut input), state: super::State(state), } } #[test] fn test_label_details_wrong_name() { let input = &b"failure: test_name [\n"[..]; let mut state = Some("other_name"); let mut input = stream(&input, &mut state); // parses as an error, which the line based iterator will catch let err = super::parse_label_details(&mut input).unwrap_err(); assert!(matches!(err, ErrMode::Cut(_))); } #[test] fn test_label_details_bracketed() { let input = &b"label [\nfoo\n]\n"[..]; let mut state = Some("label"); let mut input = stream(&input, &mut state); let output = super::parse_label_details(&mut input).unwrap(); assert_eq!( ( "label".into(), vec![Part::new(X_TRACEBACK, TRACEBACK_NAME, "foo\n".as_bytes()),] ), output ); } #[test] fn test_label_details_bracketed_multipart() { let input = &b"label [ multipart\nContent-Type: type/sub-type;p=v\nexample1\n2\r\n122\r\n340\r\nContent-Type: type/sub-type;p=v\nexample2\n2\r\n432\r\n210\r\n]\n"[..]; let mut state = Some("label"); let mut input = stream(&input, &mut state); let output = super::parse_label_details(&mut input).unwrap(); assert_eq!( ( "label".into(), vec![ Part::new("type/sub-type;p=v", "example1", "1234".as_bytes()), Part::new("type/sub-type;p=v", "example2", "4321".as_bytes()) ] ), output ); } #[test] fn test_part_smoke() { let input = &b"Content-Type: type/sub-type;p=v\nexample\n2\r\n122\r\n340\r\n"[..]; let mut state = Some("label"); let mut input = stream(&input, &mut state); let output = super::parse_part(&mut input).unwrap(); assert_eq!( Part::new("type/sub-type;p=v", "example", &b"1234"[..]), output ); } #[test] fn test_chunk() { let input = &b"10\r\n1234567890"[..]; let mut state = Some("label"); let mut input = stream(&input, &mut state); let output = super::parse_chunk(&mut input).unwrap(); assert_eq!(&b"1234567890"[..], output); } } } enum ParseState { InTest(String), Global, } /// Parses subunit v1 by reading from an AsyncBufRead. pub struct TestProtocolServer<'a> { reader: &'a mut dyn SubunitStream, state: ParseState, } impl<'a> TestProtocolServer<'a> { async fn next(&mut self) -> Result { let mut buf = vec![]; // parse the command using winnow loop { use winnow::Partial; use winnow::{error::ErrMode, BStr, Parser}; let len = self.reader.read_until(b'\n', &mut buf).await?; if buf.is_empty() { return self.generate_end_of_stream(); } let input = BStr::new(&buf); match &self.state { ParseState::InTest(test_name) => { let test_name = Some(test_name.as_str()); let mut input = parser::Stream { input: Partial::new(input), state: parser::State(&test_name), }; let parsed = parser::parse_subunit_event_in_test.parse_next(&mut input); match parsed { Err(ErrMode::Incomplete(_n)) => { if len == 0 { // end of stream when more data was needed return self.generate_end_of_stream(); } continue; } Err(_e) => { // permanent error: provide the buffered lines as Text return Ok(Event::from_buffer(&buf)); } Ok(event) => { match event { // end of test events Event::TestSuccess(_, _) | Event::TestFailure(_, _) | Event::TestError(_, _) | Event::TestSkip(_, _) | Event::TestExpectedFailure(_, _) | Event::TestUnexpectedSuccess(_, _) => { self.state = ParseState::Global; return Ok(event); } _ => { return Ok(event); } } } } } ParseState::Global => { let test_name = None; let mut input = parser::Stream { input: Partial::new(input), state: parser::State(&test_name), }; if let Ok(event) = parser::parse_subunit_event_global.parse_next(&mut input) { match &event { Event::TestStart(test_name) => { self.state = ParseState::InTest(test_name.clone()); return Ok(event); } _ => { return Ok(event); } } } } } // Not recognized as a subunit event in the current state return Self::maybe_utf8(buf); } } fn generate_end_of_stream(&mut self) -> Result { let old_state = mem::replace(&mut self.state, ParseState::Global); if let ParseState::InTest(test_name) = old_state { let message = format!("lost connection during test '{test_name}'"); return Ok(Event::TestError( test_name, vec![Part::new(X_TRACEBACK, TRACEBACK_NAME, message.as_bytes())], )); } Ok(Event::EndOfStream) } /// Read from the stream. pub fn stream(mut self) -> impl Stream> + 'a { stream! { loop { let next = self.next().await; if matches!(next, Ok(Event::EndOfStream)) { break; } yield next; } } } fn maybe_utf8(buf: Vec) -> Result { String::from_utf8(buf) .map(Event::Text) .or_else(|e| Ok(Event::Bytes(e.into_bytes()))) } } impl std::fmt::Debug for TestProtocolServer<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("TestProtocolServer").finish() } } #[cfg(feature = "sync")] /// Construct a synchronous parser on a BufRead + Debug /// /// Returns an iterator over Events, similar to the async version but synchronous. /// This allows v1 parsing in synchronous contexts without requiring tokio runtime. pub fn parse_sync( reader: &mut dyn BufReadStream, ) -> impl Iterator> + '_ { TestProtocolServerSync { reader, state: ParseState::Global, } } #[cfg(feature = "sync")] /// Synchronous version of the subunit v1 parser pub struct TestProtocolServerSync<'a> { reader: &'a mut dyn BufReadStream, state: ParseState, } #[cfg(feature = "sync")] impl<'a> TestProtocolServerSync<'a> { fn next(&mut self) -> Result, crate::Error> { let mut buf = vec![]; // parse the command using winnow loop { use winnow::Partial; use winnow::{error::ErrMode, BStr, Parser}; let len = self.reader.read_until(b'\n', &mut buf)?; if buf.is_empty() { return Ok(Some(self.generate_end_of_stream())); } let input = BStr::new(&buf); match &self.state { ParseState::InTest(test_name) => { let test_name = Some(test_name.as_str()); let mut input = parser::Stream { input: Partial::new(input), state: parser::State(&test_name), }; let parsed = parser::parse_subunit_event_in_test.parse_next(&mut input); match parsed { Err(ErrMode::Incomplete(_n)) => { if len == 0 { // end of stream when more data was needed return Ok(Some(self.generate_end_of_stream())); } continue; } Err(_e) => { // permanent error: provide the buffered lines as Text return Ok(Some(Event::from_buffer(&buf))); } Ok(event) => { match event { // end of test events Event::TestSuccess(_, _) | Event::TestFailure(_, _) | Event::TestError(_, _) | Event::TestSkip(_, _) | Event::TestExpectedFailure(_, _) | Event::TestUnexpectedSuccess(_, _) => { self.state = ParseState::Global; return Ok(Some(event)); } _ => { return Ok(Some(event)); } } } } } ParseState::Global => { let test_name = None; let mut input = parser::Stream { input: Partial::new(input), state: parser::State(&test_name), }; if let Ok(event) = parser::parse_subunit_event_global.parse_next(&mut input) { match &event { Event::TestStart(test_name) => { self.state = ParseState::InTest(test_name.clone()); return Ok(Some(event)); } _ => { return Ok(Some(event)); } } } } } // Not recognized as a subunit event in the current state return Self::maybe_utf8(buf).map(Some); } } fn generate_end_of_stream(&mut self) -> Event { let old_state = mem::replace(&mut self.state, ParseState::Global); if let ParseState::InTest(test_name) = old_state { let message = format!("lost connection during test '{test_name}'"); return Event::TestError( test_name, vec![Part::new(X_TRACEBACK, TRACEBACK_NAME, message.as_bytes())], ); } Event::EndOfStream } fn maybe_utf8(buf: Vec) -> Result { String::from_utf8(buf) .map(Event::Text) .or_else(|e| Ok(Event::Bytes(e.into_bytes()))) } } #[cfg(feature = "sync")] impl std::fmt::Debug for TestProtocolServerSync<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("TestProtocolServerSync").finish() } } #[cfg(feature = "sync")] impl<'a> Iterator for TestProtocolServerSync<'a> { type Item = Result; fn next(&mut self) -> Option { match self.next() { Ok(Some(Event::EndOfStream)) => None, Ok(Some(event)) => Some(Ok(event)), Ok(None) => None, Err(e) => Some(Err(e)), } } } #[cfg(test)] mod tests { use chrono::NaiveDate; use tokio::io::AsyncReadExt; use tokio_stream::StreamExt; use winnow::BStr; #[cfg(feature = "sync")] use crate::io::sync::WriteInto; use crate::{ io::r#async::WriteIntoAsync, v1::{Event, Part, TRACEBACK_NAME, X_TRACEBACK}, }; async fn parse_stream(mut stream: &[u8]) -> Vec { let stream: &mut dyn super::SubunitStream = &mut stream; let parser = super::parse(stream); let events = parser .collect::, crate::Error>>() .await .unwrap(); let mut buf = vec![]; assert_eq!(0, stream.read_to_end(&mut buf).await.unwrap()); events } #[tokio::test] async fn test_story() { let stream = &b"test old mcdonald\nsuccess old mcdonald\n"[..]; let events = parse_stream(stream).await; assert_eq!( &[ Event::TestStart("old mcdonald".into()), Event::TestSuccess("old mcdonald".into(), vec![]) ], &events[..] ); } #[tokio::test] async fn test_command_in_wrong_state() { let stream = &b"success old mcdonald\n"[..]; let events = parse_stream(stream).await; assert_eq!(&[Event::Text("success old mcdonald\n".into())], &events[..]); } #[tokio::test] async fn test_story_two() { let err_msg = "foo.c:53:ERROR invalid state\n"; let stream = [ &b"test old mcdonald\n"[..], b"success old mcdonald\n", b"test bing crosby\n", b"failure bing crosby [\n", err_msg.as_bytes(), b"]\n", b"test an error\n", b"error an error\n", ]; let events = parse_stream(&stream.join(&[][..])[..]).await; assert_eq!( &[ Event::TestStart("old mcdonald".into()), Event::TestSuccess("old mcdonald".into(), vec![]), Event::TestStart("bing crosby".into()), Event::TestFailure( "bing crosby".into(), vec![Part::new(X_TRACEBACK, TRACEBACK_NAME, err_msg.as_bytes())] ), Event::TestStart("an error".into()), Event::TestError("an error".into(), vec![]) ], &events[..] ); } #[tokio::test] async fn test_start_test_variants() { for cmd in &["test", "test:", "testing", "testing:"] { let stream = format!("{cmd} old mcdonald\nsuccess old mcdonald\n"); let stream = stream.as_bytes(); let events = parse_stream(&stream).await; assert_eq!( &[ Event::TestStart("old mcdonald".into()), Event::TestSuccess("old mcdonald".into(), vec![]) ], &events[..] ); } } #[tokio::test] async fn test_indented_test_colon_ignored() { let stream = &b" test: old mcdonald\n"[..]; let events = parse_stream(&stream).await; assert_eq!(&[Event::Text(" test: old mcdonald\n".into()),], &events[..]); } fn end_keywords() -> (Vec<&'static [u8]>, Vec) { ( vec![ &b"failure a\n"[..], b"failure: a\n", b"error a\n", b"error: a\n", b"success a\n", b"success: a\n", b"successful a\n", b"successful: a\n", b"]\n", ], vec![ Event::Text("failure a\n".into()), Event::Text("failure: a\n".into()), Event::Text("error a\n".into()), Event::Text("error: a\n".into()), Event::Text("success a\n".into()), Event::Text("success: a\n".into()), Event::Text("successful a\n".into()), Event::Text("successful: a\n".into()), Event::Text("]\n".into()), ], ) } #[tokio::test] async fn end_keywords_before_test() { let (end_keywords, end_events) = end_keywords(); let events = parse_stream(&end_keywords.join(&[][..])[..]).await; assert_eq!(&end_events[..], &events[..]); } #[tokio::test] async fn test_end_keywords_in_global() { let (end_keywords, end_events) = end_keywords(); for outcome in ["success", "failure", "error"] { let outcome = format!("{} old mcdonald\n", outcome); let stream = [ &b"test old mcdonald\n"[..], outcome.as_bytes(), &end_keywords.join(&[][..])[..], ]; let events = parse_stream(&stream.join(&[][..])[..]).await; assert_eq!(&end_events[..], &events[2..]); } } #[tokio::test] async fn test_end_keywords_in_test() { let (end_keywords, end_events) = end_keywords(); let stream = [ &b"test old mcdonald\n"[..], &b"test old mcdonald\n"[..], // duplicate &end_keywords.join(&[][..])[..], b"failure old mcdonald\n", // legitimate test end ]; let events = parse_stream(&stream.join(&[][..])[..]).await; let mut expected_events = vec![ Event::TestStart("old mcdonald".into()), Event::Text("test old mcdonald\n".into()), ]; expected_events.extend_from_slice(&end_events[..]); expected_events.push(Event::TestFailure("old mcdonald".into(), vec![])); assert_eq!(&expected_events[..], &events[..]); } #[tokio::test] async fn test_keywords_in_test() { let (end_keywords, end_events) = end_keywords(); let stream = [ &b"test old mcdonald\n"[..], &b"test old mcdonald\n"[..], // duplicate &end_keywords.join(&[][..])[..], b"failure old mcdonald\n", // legitimate test end ]; let events = parse_stream(&stream.join(&[][..])[..]).await; let mut expected_events = vec![ Event::TestStart("old mcdonald".into()), Event::Text("test old mcdonald\n".into()), ]; expected_events.extend_from_slice(&end_events[..]); expected_events.push(Event::TestFailure("old mcdonald".into(), vec![])); assert_eq!(&expected_events[..], &events[..]); } #[tokio::test] async fn test_keywords_in_brackets() { let (end_keywords, _end_events) = end_keywords(); let in_details = String::from_utf8( [ &b"test old mcdonald\n"[..], // duplicate to ignore &end_keywords[..end_keywords.len() - 1].join(&[][..])[..], &b" ]\n"[..], // false ending ] .join(&[][..]), ) .unwrap(); let stream = [ &b"test old mcdonald\n"[..], &b"failure old mcdonald [\n"[..], // start a details section &in_details.as_bytes(), // details with embedded commands &b"]\n"[..], // legitimate end of details ]; let events = parse_stream(&stream.join(&[][..])[..]).await; let expected_events = vec![ Event::TestStart("old mcdonald".into()), Event::TestFailure( "old mcdonald".into(), vec![Part::new( X_TRACEBACK, TRACEBACK_NAME, in_details.as_bytes(), )], ), ]; assert_eq!(&expected_events[..], &events[..]); } #[tokio::test] async fn test_invalid_lines_passthrough() { let stream = &b"randombytes\n"[..]; let events = parse_stream(&stream).await; assert_eq!(&[Event::Text("randombytes\n".into()),], &events[..]); } #[tokio::test] async fn test_empty_stream() { let stream = &b""[..]; let events = parse_stream(&stream).await; assert_eq!(&[] as &[Event], &events[..]); } #[tokio::test] async fn test_end_stream_in_test() { let stream = [&b"test old mcdonald\n"[..]]; let events = parse_stream(&stream.join(&[][..])[..]).await; assert_eq!( &[ Event::TestStart("old mcdonald".into()), Event::TestError( "old mcdonald".into(), vec![Part::new( X_TRACEBACK, TRACEBACK_NAME, "lost connection during test 'old mcdonald'".as_bytes() )] ), ], &events[..] ); } fn variant_event(variant: &str, name: &str, parts: &[Part]) -> Event { match variant { "error" => Event::TestError(name.into(), parts.to_vec()), "failure" => Event::TestFailure(name.into(), parts.to_vec()), "success" => Event::TestSuccess(name.into(), parts.to_vec()), "skip" => Event::TestSkip(name.into(), parts.to_vec()), "xfail" => Event::TestExpectedFailure(name.into(), parts.to_vec()), "uxsuccess" => Event::TestUnexpectedSuccess(name.into(), parts.to_vec()), _ => unreachable!(), } } #[tokio::test] async fn test_end_stream_after_test() { for variant in ["error", "failure", "success", "skip", "xfail", "uxsuccess"] { let input = &format!("{variant} old mcdonald\n"); let stream = [&b"test old mcdonald\n"[..], input.as_bytes()]; let events = parse_stream(&stream.join(&[][..])[..]).await; assert_eq!( &[ Event::TestStart("old mcdonald".into()), variant_event(variant, "old mcdonald", &[]), ], &events[..] ); } } #[tokio::test] async fn test_empty_bracket_content() { for variant in ["error", "failure", "success", "skip", "xfail", "uxsuccess"] { let input = &format!("{variant} old mcdonald [\n]\n"); let stream = [&b"test old mcdonald\n"[..], input.as_bytes()]; let events = parse_stream(&stream.join(&[][..])[..]).await; assert_eq!( &[ Event::TestStart("old mcdonald".into()), variant_event( variant, "old mcdonald", &[Part::new(X_TRACEBACK, TRACEBACK_NAME, "".as_bytes())] ), ], &events[..] ); } } #[tokio::test] async fn test_end_stream_in_brackets() { for outcome in ["error", "failure", "success", "skip", "xfail", "uxsuccess"] { for outcome_details in ["[", "[ multipart"] { let input = &format!("{} old mcdonald {}\n", outcome, outcome_details); let stream = [&b"test old mcdonald\n"[..], input.as_bytes()]; let events = parse_stream(&stream.join(&[][..])[..]).await; assert_eq!( &[ Event::TestStart("old mcdonald".into()), Event::TestError( "old mcdonald".into(), vec![Part::new( X_TRACEBACK, TRACEBACK_NAME, "lost connection during test 'old mcdonald'".as_bytes(), )] ), ], &events[..] ); } } } #[tokio::test] async fn test_progress_events() { let stream = [ &b"progress: push\n"[..], &b"progress: 23\n"[..], &b"progress: -2\n"[..], &b"progress: pop\n"[..], &b"progress: +4\n"[..], ]; let events = parse_stream(&stream.join(&[][..])[..]).await; assert_eq!( &[ Event::ProgressPush, Event::ProgressSet(23), Event::ProgressCurrent(-2), Event::ProgressPop, Event::ProgressCurrent(4), ], &events[..] ); } #[tokio::test] async fn test_tag_events() { let stream = [ &b"tags: foo bar:baz quux\n"[..], &b"tags: foo -bar:baz quux\n"[..], &b"test old mcdonald\n"[..], &b"tags: foo -bar:baz\n"[..], &b"success old mcdonald\n"[..], ]; let events = parse_stream(&stream.join(&[][..])[..]).await; assert_eq!( &[ Event::Tags(vec!["foo".into(), "bar:baz".into(), "quux".into()], vec![]), Event::Tags(vec!["foo".into(), "quux".into()], vec!["bar:baz".into()]), Event::TestStart("old mcdonald".into()), Event::Tags(vec!["foo".into()], vec!["bar:baz".into()]), Event::TestSuccess("old mcdonald".into(), vec![]) ], &events[..] ); } #[tokio::test] async fn test_time_events() { let stream = [ // set it globally &b"time: 2001-12-12 12:59:59Z\n"[..], &b"test old mcdonald\n"[..], // and of course, how long did the test take requires setting it before an outcome &b"time: 2001-12-13 12:59:59Z\n"[..], &b"success old mcdonald\n"[..], ]; let events = parse_stream(&stream.join(&[][..])[..]).await; assert_eq!( &[ Event::Time( NaiveDate::from_ymd_opt(2001, 12, 12) .unwrap() .and_hms_opt(12, 59, 59) .unwrap() .and_utc() ), Event::TestStart("old mcdonald".into()), Event::Time( NaiveDate::from_ymd_opt(2001, 12, 13) .unwrap() .and_hms_opt(12, 59, 59) .unwrap() .and_utc() ), Event::TestSuccess("old mcdonald".into(), vec![]) ], &events[..] ); } #[cfg(feature = "sync")] fn parse_stream_sync(stream: &[u8]) -> Vec { let mut stream = std::io::Cursor::new(stream); let stream: &mut dyn super::BufReadStream = &mut stream; let parser = super::parse_sync(stream); let events = parser.collect::, crate::Error>>().unwrap(); events } #[cfg(feature = "sync")] #[test] fn test_story_sync() { let stream = &b"test old mcdonald\nsuccess old mcdonald\n"[..]; let events = parse_stream_sync(stream); assert_eq!( &[ Event::TestStart("old mcdonald".into()), Event::TestSuccess("old mcdonald".into(), vec![]) ], &events[..] ); } #[cfg(feature = "sync")] #[test] fn test_command_in_wrong_state_sync() { let stream = &b"success old mcdonald\n"[..]; let events = parse_stream_sync(stream); assert_eq!(&[Event::Text("success old mcdonald\n".into())], &events[..]); } #[cfg(feature = "sync")] #[test] fn test_story_two_sync() { let err_msg = "foo.c:53:ERROR invalid state\n"; let stream = [ &b"test old mcdonald\n"[..], b"success old mcdonald\n", b"test bing crosby\n", b"failure bing crosby [\n", err_msg.as_bytes(), b"]\n", b"test an error\n", b"error an error\n", ]; let events = parse_stream_sync(&stream.join(&[][..])[..]); assert_eq!( &[ Event::TestStart("old mcdonald".into()), Event::TestSuccess("old mcdonald".into(), vec![]), Event::TestStart("bing crosby".into()), Event::TestFailure( "bing crosby".into(), vec![Part::new(X_TRACEBACK, TRACEBACK_NAME, err_msg.as_bytes())] ), Event::TestStart("an error".into()), Event::TestError("an error".into(), vec![]) ], &events[..] ); } #[cfg(feature = "sync")] #[test] fn test_start_test_variants_sync() { for cmd in &["test", "test:", "testing", "testing:"] { let stream = format!("{cmd} old mcdonald\nsuccess old mcdonald\n"); let stream = stream.as_bytes(); let events = parse_stream_sync(&stream); assert_eq!( &[ Event::TestStart("old mcdonald".into()), Event::TestSuccess("old mcdonald".into(), vec![]) ], &events[..] ); } } #[cfg(feature = "sync")] #[test] fn test_progress_events_sync() { let stream = [ &b"progress: push\n"[..], &b"progress: 23\n"[..], &b"progress: -2\n"[..], &b"progress: pop\n"[..], &b"progress: +4\n"[..], ]; let events = parse_stream_sync(&stream.join(&[][..])[..]); assert_eq!( &[ Event::ProgressPush, Event::ProgressSet(23), Event::ProgressCurrent(-2), Event::ProgressPop, Event::ProgressCurrent(4), ], &events[..] ); } #[cfg(feature = "sync")] #[test] fn test_tag_events_sync() { let stream = [ &b"tags: foo bar:baz quux\n"[..], &b"tags: foo -bar:baz quux\n"[..], &b"test old mcdonald\n"[..], &b"tags: foo -bar:baz\n"[..], &b"success old mcdonald\n"[..], ]; let events = parse_stream_sync(&stream.join(&[][..])[..]); assert_eq!( &[ Event::Tags(vec!["foo".into(), "bar:baz".into(), "quux".into()], vec![]), Event::Tags(vec!["foo".into(), "quux".into()], vec!["bar:baz".into()]), Event::TestStart("old mcdonald".into()), Event::Tags(vec!["foo".into()], vec!["bar:baz".into()]), Event::TestSuccess("old mcdonald".into(), vec![]) ], &events[..] ); } #[cfg(feature = "sync")] #[test] fn test_time_events_sync() { let stream = [ // set it globally &b"time: 2001-12-12 12:59:59Z\n"[..], &b"test old mcdonald\n"[..], // and of course, how long did the test take requires setting it before an outcome &b"time: 2001-12-13 12:59:59Z\n"[..], &b"success old mcdonald\n"[..], ]; let events = parse_stream_sync(&stream.join(&[][..])[..]); assert_eq!( &[ Event::Time( NaiveDate::from_ymd_opt(2001, 12, 12) .unwrap() .and_hms_opt(12, 59, 59) .unwrap() .and_utc() ), Event::TestStart("old mcdonald".into()), Event::Time( NaiveDate::from_ymd_opt(2001, 12, 13) .unwrap() .and_hms_opt(12, 59, 59) .unwrap() .and_utc() ), Event::TestSuccess("old mcdonald".into(), vec![]) ], &events[..] ); } #[cfg(feature = "sync")] #[test] fn test_empty_stream_sync() { let stream = &b""[..]; let events = parse_stream_sync(&stream); assert_eq!(&[] as &[Event], &events[..]); } #[cfg(feature = "sync")] #[test] fn test_end_stream_in_test_sync() { let stream = [&b"test old mcdonald\n"[..]]; let events = parse_stream_sync(&stream.join(&[][..])[..]); assert_eq!( &[ Event::TestStart("old mcdonald".into()), Event::TestError( "old mcdonald".into(), vec![Part::new( X_TRACEBACK, TRACEBACK_NAME, "lost connection during test 'old mcdonald'".as_bytes() )] ), ], &events[..] ); } #[cfg(feature = "sync")] #[test] fn test_invalid_lines_passthrough_sync() { let stream = &b"randombytes\n"[..]; let events = parse_stream_sync(&stream); assert_eq!(&[Event::Text("randombytes\n".into()),], &events[..]); } #[cfg(feature = "sync")] #[test] fn test_end_stream_after_test_sync() { for variant in ["error", "failure", "success", "skip", "xfail", "uxsuccess"] { let input = &format!("{variant} old mcdonald\n"); let stream = [&b"test old mcdonald\n"[..], input.as_bytes()]; let events = parse_stream_sync(&stream.join(&[][..])[..]); assert_eq!( &[ Event::TestStart("old mcdonald".into()), variant_event(variant, "old mcdonald", &[]), ], &events[..] ); } } #[cfg(feature = "sync")] #[test] fn test_empty_bracket_content_sync() { for variant in ["error", "failure", "success", "skip", "xfail", "uxsuccess"] { let input = &format!("{variant} old mcdonald [\n]\n"); let stream = [&b"test old mcdonald\n"[..], input.as_bytes()]; let events = parse_stream_sync(&stream.join(&[][..])[..]); assert_eq!( &[ Event::TestStart("old mcdonald".into()), variant_event( variant, "old mcdonald", &[Part::new(X_TRACEBACK, TRACEBACK_NAME, "".as_bytes())] ), ], &events[..] ); } } #[cfg(feature = "sync")] #[test] fn test_end_stream_in_brackets_sync() { for outcome in ["error", "failure", "success", "skip", "xfail", "uxsuccess"] { for outcome_details in ["[", "[ multipart"] { let input = &format!("{} old mcdonald {}\n", outcome, outcome_details); let stream = [&b"test old mcdonald\n"[..], input.as_bytes()]; let events = parse_stream_sync(&stream.join(&[][..])[..]); assert_eq!( &[ Event::TestStart("old mcdonald".into()), Event::TestError( "old mcdonald".into(), vec![Part::new( X_TRACEBACK, TRACEBACK_NAME, "lost connection during test 'old mcdonald'".as_bytes(), )] ), ], &events[..] ); } } } #[cfg(feature = "sync")] #[test] fn round_trip_sync() { let stream = [ &b"time: 2001-12-12 12:59:59Z\n"[..], &b"tags: foo bar:baz -quux\n"[..], &b"test: old mcdonald\n"[..], &b"success: old mcdonald\n"[..], &b"progress: push\n"[..], &b"progress: 23\n"[..], &b"progress: -2\n"[..], &b"progress: pop\n"[..], &b"test: old mcdonald\n"[..], &b"success: old mcdonald [ multipart\n"[..], &b"Content-Type: type/sub-type;p=v\n"[..], &b"example1\n"[..], &b"4\r\n12340\r\n"[..], &b"]\n"[..], &b"test: old mcdonald\n"[..], &b"success: old mcdonald [ multipart\n"[..], &b"Content-Type: simple/text\n"[..], &b"example1\n"[..], &b"0\r\n"[..], &b"]\n"[..], ]; let input = stream.join(&[][..]); let events = parse_stream_sync(&input); // sync write let mut output = vec![]; for event in &events { ::write_into(&event, &mut output).unwrap(); } assert_eq!(BStr::new(&input), BStr::new(&output)); } #[tokio::test] async fn round_trip() { let stream = [ &b"time: 2001-12-12 12:59:59Z\n"[..], &b"tags: foo bar:baz -quux\n"[..], &b"test: old mcdonald\n"[..], &b"success: old mcdonald\n"[..], &b"progress: push\n"[..], &b"progress: 23\n"[..], &b"progress: -2\n"[..], &b"progress: pop\n"[..], // &b"test: old mcdonald\n"[..], // always serialises as multipart // &b"success: old mcdonald [\n"[..], // &b"foo\n"[..], // &b"]\n"[..], &b"test: old mcdonald\n"[..], &b"success: old mcdonald [ multipart\n"[..], &b"Content-Type: type/sub-type;p=v\n"[..], &b"example1\n"[..], &b"4\r\n12340\r\n"[..], &b"]\n"[..], &b"test: old mcdonald\n"[..], &b"success: old mcdonald [ multipart\n"[..], &b"Content-Type: simple/text\n"[..], &b"example1\n"[..], &b"0\r\n"[..], &b"]\n"[..], ]; let input = stream.join(&[][..]); let events = parse_stream(&input).await; // sync #[cfg(feature = "sync")] { let mut output = vec![]; for event in &events { ::write_into(&event, &mut output).unwrap(); } assert_eq!(BStr::new(&input), BStr::new(&output)); } // async let mut output = vec![]; for event in events { ::write_into(&event, &mut output) .await .unwrap(); } assert_eq!(BStr::new(&input), BStr::new(&output)); } }