diff --git a/.editorconfig b/.editorconfig index 7a22e08..e637dc0 100644 --- a/.editorconfig +++ b/.editorconfig @@ -16,5 +16,5 @@ indent_size = 2 [*.css] indent_size = 2 -[*.{json,json5,js}] +[*.{json,json5,js,sh}] indent_size = 2 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..c97b87f --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +* text=auto +Cargo.lock -diff + +# Don't export/archive these files +.github export-ignore diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d3a2a14..c8a2602 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,6 +42,8 @@ jobs: features: - [] - [tracing] + - [strict] + - [tracing, strict] steps: - uses: actions/checkout@v4 - name: Setup Rust @@ -53,13 +55,16 @@ jobs: ~/.cargo/registry ~/.cargo/git target - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.toml') }}-${{ matrix.rust-toolchain }}-${{ join(matrix.features, ',') }} - - name: Run + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}-${{ matrix.rust-toolchain }}-${{ join(matrix.features, '-') }} + - name: Unit tests if: matrix.features[0] != null run: cargo test -p svg-path-cst --features ${{ join(matrix.features, ',') }} - - name: Run + - name: Unit tests if: matrix.features[0] == null run: cargo test -p svg-path-cst + - name: Integration tests + if: matrix.os != 'windows-latest' + run: sh src/tests/integration.sh test-release-crate: name: Test crate release diff --git a/.gitignore b/.gitignore index 81685b0..51dfe1c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ target/ -Cargo.lock .prettier-cache *.svg *.data -*.data.old \ No newline at end of file +*.data.old diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4030069..1f63877 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,6 +7,25 @@ repos: - --cache - --cache-location=.prettier-cache - --ignore-path=.gitignore + - repo: meta + hooks: + - id: check-hooks-apply + name: check-hooks-apply + - id: check-useless-excludes + name: check-useless-excludes + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: trailing-whitespace + name: trailing-whitespace + - id: end-of-file-fixer + name: end-of-file-fixer + exclude: fuzz/ + - repo: https://github.com/DavidAnson/markdownlint-cli2 + rev: v0.13.0 + hooks: + - id: markdownlint-cli2 + exclude: ^LICENSE\.md$ - repo: https://github.com/doublify/pre-commit-rust rev: v1.0 hooks: @@ -50,6 +69,26 @@ repos: -D, clippy::semicolon_if_nothing_returned, ] + - id: clippy + alias: clippy-strict + name: clippy-strict + args: + [ + --features=strict, + --, + -D, + warnings, + -D, + clippy::perf, + -D, + clippy::print_stdout, + -D, + clippy::explicit_iter_loop, + -D, + clippy::uninlined_format_args, + -D, + clippy::semicolon_if_nothing_returned, + ] - repo: https://github.com/mondeja/rust-pc-hooks rev: v1.2.0 hooks: diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..c1a9a81 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,884 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "anstyle" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "clap" +version = "4.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed6719fffa43d0d87e5fd8caeab59be1554fb028cd30edc88fc4369b17971019" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6" +dependencies = [ + "anstyle", + "clap_lex", +] + +[[package]] +name = "clap_lex" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" + +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "is-terminal", + "itertools", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "half" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" +dependencies = [ + "cfg-if", + "crunchy", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" + +[[package]] +name = "is-terminal" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "js-sys" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.158" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[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.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "oorandom" +version = "11.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9" + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "plotters" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15b6eccb8484002195a3e44fe65a4ce8e93a625797a063735536fd59cb01cf3" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "414cec62c6634ae900ea1c56128dfe87cf63e7caece0852ec76aba307cebadb7" + +[[package]] +name = "plotters-svg" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81b30686a7d9c3e010b84284bdd26a29f2138574f52f5eb6f794fc0ad924e705" +dependencies = [ + "plotters-backend", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "regex" +version = "1.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.7", + "regex-syntax 0.8.4", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.4", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "serde" +version = "1.0.209" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.209" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8043c06d9f82bd7271361ed64f415fe5e12a77fdb52e573e7f06a516dea329ad" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "snafu" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b835cb902660db3415a672d862905e791e54d306c6e8189168c7f3d9ae1c79d" +dependencies = [ + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d1e02fca405f6280643174a50c942219f0bbf4dbf7d480f1dd864d6f211ae5" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "svg-path-cst" +version = "0.1.3" +dependencies = [ + "criterion", + "doc-comment", + "snafu", + "tracing", +] + +[[package]] +name = "syn" +version = "2.0.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578e081a14e0cefc3279b0472138c513f37b41a08d5a3cca9b6e4e8ceb6cd525" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-appender" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" +dependencies = [ + "crossbeam-channel", + "thiserror", + "time", + "tracing-subscriber", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tracing-example" +version = "0.1.0" +dependencies = [ + "svg-path-cst", + "tracing", + "tracing-appender", + "tracing-subscriber", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "thread_local", + "tracing", + "tracing-core", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" + +[[package]] +name = "web-sys" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/Cargo.toml b/Cargo.toml index 26ef94b..b1375ab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "svg-path-cst" -version = "0.1.2" +version = "0.1.3" edition = "2021" readme = "README.md" description = "CST SVG path parser." @@ -18,15 +18,21 @@ path = "src/main.rs" [features] tracing = ["dep:tracing"] +strict = [] [dependencies] snafu = { version = "0.8", default-features = false } -tracing = { version = "0.1", optional = true, default-features = false, features = ["attributes"]} +tracing = { version = "0.1", optional = true, default-features = false, features = [ + "attributes" +] } [dev-dependencies] -doc-comment = "0.3.3" +doc-comment = "0.3" criterion = { version = "0.5", features = ["html_reports"] } [[bench]] -name = "performance" +name = "no_features" harness = false + +[workspace] +members = [".", "examples/tracing"] diff --git a/README.md b/README.md index 22a0626..34602c3 100644 --- a/README.md +++ b/README.md @@ -96,10 +96,29 @@ assert_eq!(cst, Ok(expected_cst)); ## Compatibility -This crate is compatible with SVG v1.1 paths, as defined in the [W3C SVG 1.1](https://www.w3.org/TR/SVG11/paths.html#PathData) and [`no_std` environments](https://docs.rust-embedded.org/book/intro/no-std.html). +This crate is compatible with SVG v1.1 paths, as defined in the [W3C SVG 1.1] +and [`no_std` environments]. ## Features -- **`tracing`**: Adds [`tracing`] support. +### **`tracing`** +Add [`tracing`] support. See the [`tracing` example] to learn how to use it. + +### **`strict`** + +Enable strict mode. The differences between strict and non-strict modes are: + +- With empty input (`b""`), non-strict mode returns an empty vector, while + strict mode returns a `SyntaxError::UnexpectedEnding` error. +- With the input `b"none"`, non-strict mode returns a `SVGPathCSTNode::None` + node, while strict mode returns a `SyntaxError::ExpectedMovetoCommand` error. + The `"none"` input is defined + [by the SVG specification](https://www.w3.org/TR/SVG/paths.html#TheDProperty). +- With input containing only whitespaces, non-strict mode returns an empty vector, + while strict mode returns a `SyntaxError::ExpectedMovetoCommand` error. + +[W3C SVG 1.1]: https://www.w3.org/TR/SVG11/paths.html#PathData +[`no_std` environments]: https://docs.rust-embedded.org/book/intro/no-std.html [`tracing`]: https://docs.rs/tracing/latest/tracing +[`tracing` example]: https://github.com/mondeja/svg-path-cst/tree/master/examples/tracing diff --git a/benches/performance.rs b/benches/no_features.rs similarity index 100% rename from benches/performance.rs rename to benches/no_features.rs diff --git a/examples/tracing/Cargo.toml b/examples/tracing/Cargo.toml new file mode 100644 index 0000000..2fc6929 --- /dev/null +++ b/examples/tracing/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "tracing-example" +version = "0.1.0" +edition = "2021" + +[dependencies] +svg-path-cst = { path = "../..", features = ["tracing"] } +tracing = { version = "0.1", default-features = false } +tracing-appender = "0.2" +tracing-subscriber = { version = "0.3", default-features = false, features = [ + "env-filter", + "ansi", +] } diff --git a/examples/tracing/src/main.rs b/examples/tracing/src/main.rs new file mode 100644 index 0000000..8cac582 --- /dev/null +++ b/examples/tracing/src/main.rs @@ -0,0 +1,23 @@ +fn init_tracing() -> tracing_appender::non_blocking::WorkerGuard { + let (non_blocking, guard) = tracing_appender::non_blocking(std::io::stdout()); + let filter = tracing_subscriber::EnvFilter::builder() + .with_default_directive(tracing::metadata::LevelFilter::TRACE.into()) + .from_env() + .unwrap(); + + tracing_subscriber::fmt() + .with_writer(non_blocking) + .with_env_filter(filter) + .with_target(false) + .with_file(false) + .with_thread_ids(true) + .with_span_events(tracing_subscriber::fmt::format::FmtSpan::ACTIVE) + .init(); + guard +} + +fn main() { + let _tracing_guard = init_tracing(); + let path = include_bytes!("../../../fuzz/corpus/simpleicons.txt"); + _ = svg_path_cst::svg_path_cst(path).unwrap(); +} diff --git a/shunit2 b/shunit2 new file mode 100644 index 0000000..fd7dc2c --- /dev/null +++ b/shunit2 @@ -0,0 +1,1463 @@ +#! /bin/sh +# vim:et:ft=sh:sts=2:sw=2 +# +# shUnit2 -- Unit testing framework for Unix shell scripts. +# +# Copyright 2008-2021 Kate Ward. All Rights Reserved. +# Released under the Apache 2.0 license. +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Author: kate.ward@forestent.com (Kate Ward) +# https://github.com/kward/shunit2 +# +# shUnit2 is a xUnit based unit test framework for Bourne shell scripts. It is +# based on the popular JUnit unit testing framework for Java. +# +# `expr` may be antiquated, but it is the only solution in some cases. +# shellcheck disable=SC2003 +# Allow usage of legacy backticked `...` notation instead of $(...). +# shellcheck disable=SC2006 + +# Return if shunit2 already loaded. +if test -n "${SHUNIT_VERSION:-}"; then + exit 0 +fi +SHUNIT_VERSION='2.1.9pre' + +# Return values that scripts can use. +SHUNIT_TRUE=0 +SHUNIT_FALSE=1 +SHUNIT_ERROR=2 + +# Determine if `builtin` command exists. +__SHUNIT_BUILTIN='builtin' +# shellcheck disable=2039 +if ! ("${__SHUNIT_BUILTIN}" echo 123 >/dev/null 2>&1); then + __SHUNIT_BUILTIN='' +fi + +# Determine some reasonable command defaults. +__SHUNIT_CMD_ECHO_ESC='echo -e' +# shellcheck disable=SC2039,SC3037 +if ${__SHUNIT_BUILTIN} [ "`echo -e test`" = '-e test' ]; then + __SHUNIT_CMD_ECHO_ESC='echo' +fi + +# Commands a user can override if needed. +__SHUNIT_CMD_TPUT='tput' +SHUNIT_CMD_TPUT=${SHUNIT_CMD_TPUT:-${__SHUNIT_CMD_TPUT}} + +# Enable color output. Options are 'auto', 'always', or 'never'. +SHUNIT_COLOR=${SHUNIT_COLOR:-auto} + +# +# Internal constants. +# + +__SHUNIT_MODE_SOURCED='sourced' +__SHUNIT_MODE_STANDALONE='standalone' +__SHUNIT_PARENT=${SHUNIT_PARENT:-$0} + +# User provided test prefix to display in front of the name of the test being +# executed. Define by setting the SHUNIT_TEST_PREFIX variable. +__SHUNIT_TEST_PREFIX=${SHUNIT_TEST_PREFIX:-} + +# ANSI colors. +__SHUNIT_ANSI_NONE='\033[0m' +__SHUNIT_ANSI_RED='\033[1;31m' +__SHUNIT_ANSI_GREEN='\033[1;32m' +__SHUNIT_ANSI_YELLOW='\033[1;33m' +__SHUNIT_ANSI_CYAN='\033[1;36m' + +# +# Internal variables. +# + +# Variables. +__shunit_lineno='' # Line number of executed test. +__shunit_mode=${__SHUNIT_MODE_SOURCED} # Operating mode. +__shunit_reportGenerated=${SHUNIT_FALSE} # Is report generated. +__shunit_script='' # Filename of unittest script (standalone mode). +__shunit_skip=${SHUNIT_FALSE} # Is skipping enabled. +__shunit_suite='' # Suite of tests to execute. +__shunit_clean=${SHUNIT_FALSE} # _shunit_cleanup() was already called. +__shunit_suiteName='' # Text name of current test suite. +__shunit_xmlSuiteName='' # XML-ready text name of current test suite. + +# JUnit XML variables. +__shunit_junitXmlOutputFile='' # File to use for JUnit XML output in addition to stdout. +__shunit_junitXmlTestCases='' # Test cases info in the JUnit XML format for output +__shunit_junitXmlCurrentTestCaseErrors='' # Current test case error info in the JUnit XML format for output + +# ANSI colors (populated by _shunit_configureColor()). +__shunit_ansi_none='' +__shunit_ansi_red='' +__shunit_ansi_green='' +__shunit_ansi_yellow='' +__shunit_ansi_cyan='' + +# Counts of tests. +__shunit_testSuccess=${SHUNIT_TRUE} +__shunit_testsTotal=0 +__shunit_testsPassed=0 +__shunit_testsFailed=0 + +# Counts of asserts. +__shunit_assertsTotal=0 +__shunit_assertsPassed=0 +__shunit_assertsFailed=0 +__shunit_assertsSkipped=0 +__shunit_assertsCurrentTest=0 + +# +# Internal functions. +# + +# Logging. +_shunit_warn() { + ${__SHUNIT_CMD_ECHO_ESC} "${__shunit_ansi_yellow}shunit2:WARN${__shunit_ansi_none} $*" >&2 +} +_shunit_error() { + ${__SHUNIT_CMD_ECHO_ESC} "${__shunit_ansi_red}shunit2:ERROR${__shunit_ansi_none} $*" >&2 +} +_shunit_fatal() { + ${__SHUNIT_CMD_ECHO_ESC} "${__shunit_ansi_red}shunit2:FATAL${__shunit_ansi_none} $*" >&2 + exit ${SHUNIT_ERROR} +} + +# +# Macros. +# + +# shellcheck disable=SC2016,SC2089 +_SHUNIT_LINENO_='eval __shunit_lineno=""; if ${__SHUNIT_BUILTIN} [ "${1:-}" = "--lineno" ] && ${__SHUNIT_BUILTIN} [ -n "${2:-}" ]; then __shunit_lineno="[${2}]"; shift 2; fi;' + +# +# Setup. +# + +# Specific shell checks. +if ${__SHUNIT_BUILTIN} [ -n "${ZSH_VERSION:-}" ]; then + setopt |grep "^shwordsplit$" >/dev/null + if ${__SHUNIT_BUILTIN} [ $? -ne ${SHUNIT_TRUE} ]; then + _shunit_fatal 'zsh shwordsplit option is required for proper operation' + fi + if ${__SHUNIT_BUILTIN} [ -z "${SHUNIT_PARENT:-}" ]; then + _shunit_fatal "zsh does not pass \$0 through properly. please declare \ +\"SHUNIT_PARENT=\$0\" before calling shUnit2" + fi +fi + +# Set the constants readonly. +__shunit_constants=`set |grep '^__SHUNIT_' |cut -d= -f1` +echo "${__shunit_constants}" |grep '^Binary file' >/dev/null && \ + __shunit_constants=`set |grep -a '^__SHUNIT_' |cut -d= -f1` +for __shunit_const in ${__shunit_constants}; do + if ${__SHUNIT_BUILTIN} [ -z "${ZSH_VERSION:-}" ]; then + readonly "${__shunit_const}" + else + case ${ZSH_VERSION} in + [123].*) readonly "${__shunit_const}" ;; + *) + # Declare readonly constants globally. + # shellcheck disable=SC2039,SC3045 + readonly -g "${__shunit_const}" + esac + fi +done +unset __shunit_const __shunit_constants + +#----------------------------------------------------------------------------- +# Assertion functions. +# + +# Assert that two values are equal to one another. +# +# Args: +# message: string: failure message [optional] +# expected: string: expected value +# actual: string: actual value +# Returns: +# integer: success (TRUE/FALSE/ERROR constant) +assertEquals() { + # shellcheck disable=SC2090 + ${_SHUNIT_LINENO_} + if ${__SHUNIT_BUILTIN} [ $# -lt 2 -o $# -gt 3 ]; then + _shunit_error "assertEquals() requires two or three arguments; $# given" + _shunit_assertFail + return ${SHUNIT_ERROR} + fi + if _shunit_shouldSkip; then + return ${SHUNIT_TRUE} + fi + + shunit_message_=${__shunit_lineno} + if ${__SHUNIT_BUILTIN} [ $# -eq 3 ]; then + shunit_message_="${shunit_message_}$1" + shift + fi + shunit_expected_=$1 + shunit_actual_=$2 + + shunit_return=${SHUNIT_TRUE} + if ${__SHUNIT_BUILTIN} [ "${shunit_expected_}" = "${shunit_actual_}" ]; then + _shunit_assertPass + else + failNotEquals "${shunit_message_}" "${shunit_expected_}" "${shunit_actual_}" + shunit_return=${SHUNIT_FALSE} + fi + + unset shunit_message_ shunit_expected_ shunit_actual_ + return ${shunit_return} +} +# shellcheck disable=SC2016,SC2034 +_ASSERT_EQUALS_='eval assertEquals --lineno "${LINENO:-}"' + +# Assert that two values are not equal to one another. +# +# Args: +# message: string: failure message [optional] +# expected: string: expected value +# actual: string: actual value +# Returns: +# integer: success (TRUE/FALSE/ERROR constant) +assertNotEquals() { + # shellcheck disable=SC2090 + ${_SHUNIT_LINENO_} + if ${__SHUNIT_BUILTIN} [ $# -lt 2 -o $# -gt 3 ]; then + _shunit_error "assertNotEquals() requires two or three arguments; $# given" + _shunit_assertFail + return ${SHUNIT_ERROR} + fi + if _shunit_shouldSkip; then + return ${SHUNIT_TRUE} + fi + + shunit_message_=${__shunit_lineno} + if ${__SHUNIT_BUILTIN} [ $# -eq 3 ]; then + shunit_message_="${shunit_message_}$1" + shift + fi + shunit_expected_=$1 + shunit_actual_=$2 + + shunit_return=${SHUNIT_TRUE} + if ${__SHUNIT_BUILTIN} [ "${shunit_expected_}" != "${shunit_actual_}" ]; then + _shunit_assertPass + else + failSame "${shunit_message_}" "${shunit_expected_}" "${shunit_actual_}" + shunit_return=${SHUNIT_FALSE} + fi + + unset shunit_message_ shunit_expected_ shunit_actual_ + return ${shunit_return} +} +# shellcheck disable=SC2016,SC2034 +_ASSERT_NOT_EQUALS_='eval assertNotEquals --lineno "${LINENO:-}"' + +# Assert that a container contains a content. +# +# Args: +# message: string: failure message [optional] +# container: string: container to analyze +# content: string: content to find +# Returns: +# integer: success (TRUE/FALSE/ERROR constant) +assertContains() { + # shellcheck disable=SC2090 + ${_SHUNIT_LINENO_} + if ${__SHUNIT_BUILTIN} [ $# -lt 2 -o $# -gt 3 ]; then + _shunit_error "assertContains() requires two or three arguments; $# given" + _shunit_assertFail + return ${SHUNIT_ERROR} + fi + if _shunit_shouldSkip; then + return ${SHUNIT_TRUE} + fi + + shunit_message_=${__shunit_lineno} + if ${__SHUNIT_BUILTIN} [ $# -eq 3 ]; then + shunit_message_="${shunit_message_}$1" + shift + fi + shunit_container_=$1 + shunit_content_=$2 + shunit_return=${SHUNIT_TRUE} + if echo "${shunit_container_}" |grep -F -- "${shunit_content_}" >/dev/null; then + _shunit_assertPass + else + failNotFound "${shunit_message_}" "${shunit_content_}" + shunit_return=${SHUNIT_FALSE} + fi + + unset shunit_message_ shunit_container_ shunit_content_ + return ${shunit_return} +} +# shellcheck disable=SC2016,SC2034 +_ASSERT_CONTAINS_='eval assertContains --lineno "${LINENO:-}"' + +# Assert that a container does not contain a content. +# +# Args: +# message: string: failure message [optional] +# container: string: container to analyze +# content: string: content to look for +# Returns: +# integer: success (TRUE/FALSE/ERROR constant) +assertNotContains() { + # shellcheck disable=SC2090 + ${_SHUNIT_LINENO_} + if ${__SHUNIT_BUILTIN} [ $# -lt 2 -o $# -gt 3 ]; then + _shunit_error "assertNotContains() requires two or three arguments; $# given" + _shunit_assertFail + return ${SHUNIT_ERROR} + fi + if _shunit_shouldSkip; then + return ${SHUNIT_TRUE} + fi + + shunit_message_=${__shunit_lineno} + if ${__SHUNIT_BUILTIN} [ $# -eq 3 ]; then + shunit_message_="${shunit_message_}$1" + shift + fi + shunit_container_=$1 + shunit_content_=$2 + + shunit_return=${SHUNIT_TRUE} + if echo "$shunit_container_" |grep -F -- "$shunit_content_" > /dev/null; then + failFound "${shunit_message_}" "${shunit_content_}" + shunit_return=${SHUNIT_FALSE} + else + _shunit_assertPass + fi + + unset shunit_message_ shunit_container_ shunit_content_ + return ${shunit_return} +} +# shellcheck disable=SC2016,SC2034 +_ASSERT_NOT_CONTAINS_='eval assertNotContains --lineno "${LINENO:-}"' + +# Assert that a value is null (i.e. an empty string). +# +# Args: +# message: string: failure message [optional] +# actual: string: actual value +# Returns: +# integer: success (TRUE/FALSE/ERROR constant) +assertNull() { + # shellcheck disable=SC2090 + ${_SHUNIT_LINENO_} + if ${__SHUNIT_BUILTIN} [ $# -gt 2 ]; then + # Allowing 0 arguments as $1 might actually be null. + _shunit_error "assertNull() requires one or two arguments; $# given" + _shunit_assertFail + return ${SHUNIT_ERROR} + fi + if _shunit_shouldSkip; then + return ${SHUNIT_TRUE} + fi + + shunit_message_=${__shunit_lineno} + if ${__SHUNIT_BUILTIN} [ $# -eq 2 ]; then + shunit_message_="${shunit_message_}$1" + shift + fi + + ${__SHUNIT_BUILTIN} test -z "${1:-}" + assertTrue "${shunit_message_}" $? + shunit_return=$? + + unset shunit_message_ + return ${shunit_return} +} +# shellcheck disable=SC2016,SC2034 +_ASSERT_NULL_='eval assertNull --lineno "${LINENO:-}"' + +# Assert that a value is not null (i.e. a non-empty string). +# +# Args: +# message: string: failure message [optional] +# actual: string: actual value +# Returns: +# integer: success (TRUE/FALSE/ERROR constant) +assertNotNull() { + # shellcheck disable=SC2090 + ${_SHUNIT_LINENO_} + if ${__SHUNIT_BUILTIN} [ $# -gt 2 ]; then + # Allowing 0 arguments as $1 might actually be null. + _shunit_error "assertNotNull() requires one or two arguments; $# given" + _shunit_assertFail + return ${SHUNIT_ERROR} + fi + if _shunit_shouldSkip; then + return ${SHUNIT_TRUE} + fi + + shunit_message_=${__shunit_lineno} + if ${__SHUNIT_BUILTIN} [ $# -eq 2 ]; then + shunit_message_="${shunit_message_}$1" + shift + fi + + ${__SHUNIT_BUILTIN} test -n "${1:-}" + assertTrue "${shunit_message_}" $? + shunit_return=$? + + unset shunit_message_ + return ${shunit_return} +} +# shellcheck disable=SC2016,SC2034 +_ASSERT_NOT_NULL_='eval assertNotNull --lineno "${LINENO:-}"' + +# Assert that two values are the same (i.e. equal to one another). +# +# Args: +# message: string: failure message [optional] +# expected: string: expected value +# actual: string: actual value +# Returns: +# integer: success (TRUE/FALSE/ERROR constant) +assertSame() { + # shellcheck disable=SC2090 + ${_SHUNIT_LINENO_} + if ${__SHUNIT_BUILTIN} [ $# -lt 2 -o $# -gt 3 ]; then + _shunit_error "assertSame() requires two or three arguments; $# given" + _shunit_assertFail + return ${SHUNIT_ERROR} + fi + if _shunit_shouldSkip; then + return ${SHUNIT_TRUE} + fi + + shunit_message_=${__shunit_lineno} + if ${__SHUNIT_BUILTIN} [ $# -eq 3 ]; then + shunit_message_="${shunit_message_}$1" + shift + fi + assertEquals "${shunit_message_}" "$1" "$2" + shunit_return=$? + + unset shunit_message_ + return ${shunit_return} +} +# shellcheck disable=SC2016,SC2034 +_ASSERT_SAME_='eval assertSame --lineno "${LINENO:-}"' + +# Assert that two values are not the same (i.e. not equal to one another). +# +# Args: +# message: string: failure message [optional] +# expected: string: expected value +# actual: string: actual value +# Returns: +# integer: success (TRUE/FALSE/ERROR constant) +assertNotSame() { + # shellcheck disable=SC2090 + ${_SHUNIT_LINENO_} + if ${__SHUNIT_BUILTIN} [ $# -lt 2 -o $# -gt 3 ]; then + _shunit_error "assertNotSame() requires two or three arguments; $# given" + _shunit_assertFail + return ${SHUNIT_ERROR} + fi + if _shunit_shouldSkip; then + return ${SHUNIT_TRUE} + fi + + shunit_message_=${__shunit_lineno} + if ${__SHUNIT_BUILTIN} [ $# -eq 3 ]; then + shunit_message_="${shunit_message_:-}$1" + shift + fi + assertNotEquals "${shunit_message_}" "$1" "$2" + shunit_return=$? + + unset shunit_message_ + return ${shunit_return} +} +# shellcheck disable=SC2016,SC2034 +_ASSERT_NOT_SAME_='eval assertNotSame --lineno "${LINENO:-}"' + +# Assert that a value or shell test condition is true. +# +# In shell, a value of 0 is true and a non-zero value is false. Any integer +# value passed can thereby be tested. +# +# Shell supports much more complicated tests though, and a means to support +# them was needed. As such, this function tests that conditions are true or +# false through evaluation rather than just looking for a true or false. +# +# The following test will succeed: +# assertTrue 0 +# assertTrue "[ 34 -gt 23 ]" +# The following test will fail with a message: +# assertTrue 123 +# assertTrue "test failed" "[ -r '/non/existent/file' ]" +# +# Args: +# message: string: failure message [optional] +# condition: string: integer value or shell conditional statement +# Returns: +# integer: success (TRUE/FALSE/ERROR constant) +assertTrue() { + # shellcheck disable=SC2090 + ${_SHUNIT_LINENO_} + if ${__SHUNIT_BUILTIN} [ $# -lt 1 -o $# -gt 2 ]; then + _shunit_error "assertTrue() takes one or two arguments; $# given" + _shunit_assertFail + return ${SHUNIT_ERROR} + fi + if _shunit_shouldSkip; then + return ${SHUNIT_TRUE} + fi + + shunit_message_=${__shunit_lineno} + if ${__SHUNIT_BUILTIN} [ $# -eq 2 ]; then + shunit_message_="${shunit_message_}$1" + shift + fi + shunit_condition_=$1 + + # See if condition is an integer, i.e. a return value. + shunit_return=${SHUNIT_TRUE} + if ${__SHUNIT_BUILTIN} [ -z "${shunit_condition_}" ]; then + # Null condition. + shunit_return=${SHUNIT_FALSE} + elif (expr \( "${shunit_condition_}" + '0' \) '=' "${shunit_condition_}" >/dev/null 2>&1) + then + # Possible return value. Treating 0 as true, and non-zero as false. + if ${__SHUNIT_BUILTIN} [ "${shunit_condition_}" -ne 0 ]; then + shunit_return=${SHUNIT_FALSE} + fi + else + # Hopefully... a condition. + if ! eval "${shunit_condition_}" >/dev/null 2>&1; then + shunit_return=${SHUNIT_FALSE} + fi + fi + + # Record the test. + if ${__SHUNIT_BUILTIN} [ ${shunit_return} -eq ${SHUNIT_TRUE} ]; then + _shunit_assertPass + else + _shunit_assertFail "${shunit_message_}" + fi + + unset shunit_message_ shunit_condition_ + return ${shunit_return} +} +# shellcheck disable=SC2016,SC2034 +_ASSERT_TRUE_='eval assertTrue --lineno "${LINENO:-}"' + +# Assert that a value or shell test condition is false. +# +# In shell, a value of 0 is true and a non-zero value is false. Any integer +# value passed can thereby be tested. +# +# Shell supports much more complicated tests though, and a means to support +# them was needed. As such, this function tests that conditions are true or +# false through evaluation rather than just looking for a true or false. +# +# The following test will succeed: +# assertFalse 1 +# assertFalse "[ 'apples' = 'oranges' ]" +# The following test will fail with a message: +# assertFalse 0 +# assertFalse "test failed" "[ 1 -eq 1 -a 2 -eq 2 ]" +# +# Args: +# message: string: failure message [optional] +# condition: string: integer value or shell conditional statement +# Returns: +# integer: success (TRUE/FALSE/ERROR constant) +assertFalse() { + # shellcheck disable=SC2090 + ${_SHUNIT_LINENO_} + if ${__SHUNIT_BUILTIN} [ $# -lt 1 -o $# -gt 2 ]; then + _shunit_error "assertFalse() requires one or two arguments; $# given" + _shunit_assertFail + return ${SHUNIT_ERROR} + fi + if _shunit_shouldSkip; then + return ${SHUNIT_TRUE} + fi + + shunit_message_=${__shunit_lineno} + if ${__SHUNIT_BUILTIN} [ $# -eq 2 ]; then + shunit_message_="${shunit_message_}$1" + shift + fi + shunit_condition_=$1 + + # See if condition is an integer, i.e. a return value. + shunit_return=${SHUNIT_TRUE} + if ${__SHUNIT_BUILTIN} [ -z "${shunit_condition_}" ]; then + # Null condition. + shunit_return=${SHUNIT_TRUE} + elif (expr \( "${shunit_condition_}" + '0' \) '=' "${shunit_condition_}" >/dev/null 2>&1); then + # Possible return value. Treating 0 as true, and non-zero as false. + if ${__SHUNIT_BUILTIN} [ "${shunit_condition_}" -eq 0 ]; then + shunit_return=${SHUNIT_FALSE} + fi + else + # Hopefully... a condition. + # shellcheck disable=SC2086 + if eval ${shunit_condition_} >/dev/null 2>&1; then + shunit_return=${SHUNIT_FALSE} + fi + fi + + # Record the test. + if ${__SHUNIT_BUILTIN} [ "${shunit_return}" -eq "${SHUNIT_TRUE}" ]; then + _shunit_assertPass + else + _shunit_assertFail "${shunit_message_}" + fi + + unset shunit_message_ shunit_condition_ + return "${shunit_return}" +} +# shellcheck disable=SC2016,SC2034 +_ASSERT_FALSE_='eval assertFalse --lineno "${LINENO:-}"' + +#----------------------------------------------------------------------------- +# Failure functions. +# + +# Records a test failure. +# +# Args: +# message: string: failure message [optional] +# Returns: +# integer: success (TRUE/FALSE/ERROR constant) +fail() { + # shellcheck disable=SC2090 + ${_SHUNIT_LINENO_} + if ${__SHUNIT_BUILTIN} [ $# -gt 1 ]; then + _shunit_error "fail() requires zero or one arguments; $# given" + return ${SHUNIT_ERROR} + fi + if _shunit_shouldSkip; then + return ${SHUNIT_TRUE} + fi + + shunit_message_=${__shunit_lineno} + if ${__SHUNIT_BUILTIN} [ $# -eq 1 ]; then + shunit_message_="${shunit_message_}$1" + shift + fi + + _shunit_assertFail "${shunit_message_}" + + unset shunit_message_ + return ${SHUNIT_FALSE} +} +# shellcheck disable=SC2016,SC2034 +_FAIL_='eval fail --lineno "${LINENO:-}"' + +# Records a test failure, stating two values were not equal. +# +# Args: +# message: string: failure message [optional] +# expected: string: expected value +# actual: string: actual value +# Returns: +# integer: success (TRUE/FALSE/ERROR constant) +failNotEquals() { + # shellcheck disable=SC2090 + ${_SHUNIT_LINENO_} + if ${__SHUNIT_BUILTIN} [ $# -lt 2 -o $# -gt 3 ]; then + _shunit_error "failNotEquals() requires one or two arguments; $# given" + return ${SHUNIT_ERROR} + fi + if _shunit_shouldSkip; then + return ${SHUNIT_TRUE} + fi + + shunit_message_=${__shunit_lineno} + if ${__SHUNIT_BUILTIN} [ $# -eq 3 ]; then + shunit_message_="${shunit_message_}$1" + shift + fi + shunit_expected_=$1 + shunit_actual_=$2 + + shunit_message_=${shunit_message_%% } + _shunit_assertFail "${shunit_message_:+${shunit_message_} }expected:<${shunit_expected_}> but was:<${shunit_actual_}>" + + unset shunit_message_ shunit_expected_ shunit_actual_ + return ${SHUNIT_FALSE} +} +# shellcheck disable=SC2016,SC2034 +_FAIL_NOT_EQUALS_='eval failNotEquals --lineno "${LINENO:-}"' + +# Records a test failure, stating a value was found. +# +# Args: +# message: string: failure message [optional] +# content: string: found value +# Returns: +# integer: success (TRUE/FALSE/ERROR constant) +failFound() { + # shellcheck disable=SC2090 + ${_SHUNIT_LINENO_} + if ${__SHUNIT_BUILTIN} [ $# -lt 1 -o $# -gt 2 ]; then + _shunit_error "failFound() requires one or two arguments; $# given" + return ${SHUNIT_ERROR} + fi + if _shunit_shouldSkip; then + return ${SHUNIT_TRUE} + fi + + shunit_message_=${__shunit_lineno} + if ${__SHUNIT_BUILTIN} [ $# -eq 2 ]; then + shunit_message_="${shunit_message_}$1" + shift + fi + shunit_content_=$1 + + shunit_message_=${shunit_message_%% } + _shunit_assertFail "${shunit_message_:+${shunit_message_} }found:<${shunit_content_}>" + + unset shunit_message_ shunit_content_ + return ${SHUNIT_FALSE} +} +# shellcheck disable=SC2016,SC2034 +_FAIL_FOUND_='eval failFound --lineno "${LINENO:-}"' + +# Records a test failure, stating a content was not found. +# +# Args: +# message: string: failure message [optional] +# content: string: content not found +# Returns: +# integer: success (TRUE/FALSE/ERROR constant) +failNotFound() { + # shellcheck disable=SC2090 + ${_SHUNIT_LINENO_} + if ${__SHUNIT_BUILTIN} [ $# -lt 1 -o $# -gt 2 ]; then + _shunit_error "failNotFound() requires one or two arguments; $# given" + return ${SHUNIT_ERROR} + fi + if _shunit_shouldSkip; then + return ${SHUNIT_TRUE} + fi + + shunit_message_=${__shunit_lineno} + if ${__SHUNIT_BUILTIN} [ $# -eq 2 ]; then + shunit_message_="${shunit_message_}$1" + shift + fi + shunit_content_=$1 + + shunit_message_=${shunit_message_%% } + _shunit_assertFail "${shunit_message_:+${shunit_message_} }not found:<${shunit_content_}>" + + unset shunit_message_ shunit_content_ + return ${SHUNIT_FALSE} +} +# shellcheck disable=SC2016,SC2034 +_FAIL_NOT_FOUND_='eval failNotFound --lineno "${LINENO:-}"' + +# Records a test failure, stating two values should have been the same. +# +# Args: +# message: string: failure message [optional] +# expected: string: expected value +# actual: string: actual value +# Returns: +# integer: success (TRUE/FALSE/ERROR constant) +failSame() { + # shellcheck disable=SC2090 + ${_SHUNIT_LINENO_} + if ${__SHUNIT_BUILTIN} [ $# -lt 2 -o $# -gt 3 ]; then + _shunit_error "failSame() requires two or three arguments; $# given" + return ${SHUNIT_ERROR} + fi + if _shunit_shouldSkip; then + return ${SHUNIT_TRUE} + fi + + shunit_message_=${__shunit_lineno} + if ${__SHUNIT_BUILTIN} [ $# -eq 3 ]; then + shunit_message_="${shunit_message_}$1" + shift + fi + + shunit_message_=${shunit_message_%% } + _shunit_assertFail "${shunit_message_:+${shunit_message_} }expected not same" + + unset shunit_message_ + return ${SHUNIT_FALSE} +} +# shellcheck disable=SC2016,SC2034 +_FAIL_SAME_='eval failSame --lineno "${LINENO:-}"' + +# Records a test failure, stating two values were not equal. +# +# This is functionally equivalent to calling failNotEquals(). +# +# Args: +# message: string: failure message [optional] +# expected: string: expected value +# actual: string: actual value +# Returns: +# integer: success (TRUE/FALSE/ERROR constant) +failNotSame() { + # shellcheck disable=SC2090 + ${_SHUNIT_LINENO_} + if ${__SHUNIT_BUILTIN} [ $# -lt 2 -o $# -gt 3 ]; then + _shunit_error "failNotSame() requires one or two arguments; $# given" + return ${SHUNIT_ERROR} + fi + if _shunit_shouldSkip; then + return ${SHUNIT_TRUE} + fi + + shunit_message_=${__shunit_lineno} + if ${__SHUNIT_BUILTIN} [ $# -eq 3 ]; then + shunit_message_="${shunit_message_}$1" + shift + fi + failNotEquals "${shunit_message_}" "$1" "$2" + shunit_return=$? + + unset shunit_message_ + return ${shunit_return} +} +# shellcheck disable=SC2016,SC2034 +_FAIL_NOT_SAME_='eval failNotSame --lineno "${LINENO:-}"' + +#----------------------------------------------------------------------------- +# Skipping functions. +# + +# Force remaining assert and fail functions to be "skipped". +# +# This function forces the remaining assert and fail functions to be "skipped", +# i.e. they will have no effect. Each function skipped will be recorded so that +# the total of asserts and fails will not be altered. +# +# Args: +# message: string: message to provide to user [optional] +startSkipping() { + if ${__SHUNIT_BUILTIN} [ $# -gt 0 ]; then _shunit_warn "[skipping] $*"; fi + __shunit_skip=${SHUNIT_TRUE} +} + +# Resume the normal recording behavior of assert and fail calls. +# +# Args: +# None +endSkipping() { __shunit_skip=${SHUNIT_FALSE}; } + +# Returns the state of assert and fail call skipping. +# +# Args: +# None +# Returns: +# boolean: (TRUE/FALSE constant) +isSkipping() { return ${__shunit_skip}; } + +#----------------------------------------------------------------------------- +# Suite functions. +# + +# Stub. This function should contains all unit test calls to be made. +# +# DEPRECATED (as of 2.1.0) +# +# This function can be optionally overridden by the user in their test suite. +# +# If this function exists, it will be called when shunit2 is sourced. If it +# does not exist, shunit2 will search the parent script for all functions +# beginning with the word 'test', and they will be added dynamically to the +# test suite. +# +# This function should be overridden by the user in their unit test suite. +# Note: see _shunit_mktempFunc() for actual implementation +# +# Args: +# None +#suite() { :; } # DO NOT UNCOMMENT THIS FUNCTION + +# Adds a function name to the list of tests schedule for execution. +# +# This function should only be called from within the suite() function. +# +# Args: +# function: string: name of a function to add to current unit test suite +suite_addTest() { + shunit_func_=${1:-} + + __shunit_suite="${__shunit_suite:+${__shunit_suite} }${shunit_func_}" + __shunit_testsTotal=`expr "${__shunit_testsTotal}" + 1` + + unset shunit_func_ +} + +# Stub. This function will be called once before any tests are run. +# +# Common one-time environment preparation tasks shared by all tests can be +# defined here. +# +# This function should be overridden by the user in their unit test suite. +# Note: see _shunit_mktempFunc() for actual implementation +# +# Args: +# None +#oneTimeSetUp() { :; } # DO NOT UNCOMMENT THIS FUNCTION + +# Stub. This function will be called once after all tests are finished. +# +# Common one-time environment cleanup tasks shared by all tests can be defined +# here. +# +# This function should be overridden by the user in their unit test suite. +# Note: see _shunit_mktempFunc() for actual implementation +# +# Args: +# None +#oneTimeTearDown() { :; } # DO NOT UNCOMMENT THIS FUNCTION + +# Stub. This function will be called before each test is run. +# +# Common environment preparation tasks shared by all tests can be defined here. +# +# This function should be overridden by the user in their unit test suite. +# Note: see _shunit_mktempFunc() for actual implementation +# +# Args: +# None +#setUp() { :; } # DO NOT UNCOMMENT THIS FUNCTION + +# Note: see _shunit_mktempFunc() for actual implementation +# Stub. This function will be called after each test is run. +# +# Common environment cleanup tasks shared by all tests can be defined here. +# +# This function should be overridden by the user in their unit test suite. +# Note: see _shunit_mktempFunc() for actual implementation +# +# Args: +# None +#tearDown() { :; } # DO NOT UNCOMMENT THIS FUNCTION + +#------------------------------------------------------------------------------ +# Internal shUnit2 functions. +# + +# Create a temporary directory to store various run-time files in. +# +# This function is a cross-platform temporary directory creation tool. Not all +# OSes have the `mktemp` function, so one is included here. +# +# Args: +# None +# Outputs: +# string: the temporary directory that was created +_shunit_mktempDir() { + # Try the standard `mktemp` function. + if ( exec mktemp -dqt shunit.XXXXXX 2>/dev/null ); then + return + fi + + # The standard `mktemp` didn't work. Use our own. + # shellcheck disable=SC2039,SC3028 + if ${__SHUNIT_BUILTIN} [ -r '/dev/urandom' -a -x '/usr/bin/od' ]; then + _shunit_random_=`/usr/bin/od -vAn -N4 -tx4 "${_shunit_file_}" +#! /bin/sh +exit ${SHUNIT_TRUE} +EOF + command chmod +x "${_shunit_file_}" + done + + unset _shunit_file_ +} + +# Final cleanup function to leave things as we found them. +# +# Besides removing the temporary directory, this function is in charge of the +# final exit code of the unit test. The exit code is based on how the script +# was ended (e.g. normal exit, or via Ctrl-C). +# +# Args: +# name: string: name of the trap called (specified when trap defined) +_shunit_cleanup() { + _shunit_name_=$1 + + _shunit_signal_=0 + case "${_shunit_name_}" in + EXIT) ;; + INT) _shunit_signal_=130 ;; # 2+128 + TERM) _shunit_signal_=143 ;; # 15+128 + *) + _shunit_error "unrecognized trap value (${_shunit_name_})" + ;; + esac + if ${__SHUNIT_BUILTIN} [ "${_shunit_name_}" != 'EXIT' ]; then + _shunit_warn "trapped and now handling the (${_shunit_name_}) signal" + fi + + # Do our work. + if ${__SHUNIT_BUILTIN} [ ${__shunit_clean} -eq ${SHUNIT_FALSE} ]; then + # Ensure tear downs are only called once. + __shunit_clean=${SHUNIT_TRUE} + + tearDown || _shunit_warn 'tearDown() returned non-zero return code.' + oneTimeTearDown || \ + _shunit_warn 'oneTimeTearDown() returned non-zero return code.' + + command rm -fr "${__shunit_tmpDir}" + fi + + if ${__SHUNIT_BUILTIN} [ "${_shunit_name_}" != 'EXIT' ]; then + # Handle all non-EXIT signals. + trap - 0 # Disable EXIT trap. + exit ${_shunit_signal_} + elif ${__SHUNIT_BUILTIN} [ ${__shunit_reportGenerated} -eq ${SHUNIT_FALSE} ]; then + _shunit_assertFail 'unknown failure encountered running a test' + _shunit_generateReport + exit ${SHUNIT_ERROR} + fi + + unset _shunit_name_ _shunit_signal_ +} + +# configureColor based on user color preference. +# +# Args: +# color: string: color mode (one of `always`, `auto`, or `never`). +_shunit_configureColor() { + _shunit_color_=${SHUNIT_FALSE} # By default, no color. + case $1 in + 'always') _shunit_color_=${SHUNIT_TRUE} ;; + 'auto') + if ${__SHUNIT_BUILTIN} [ "`_shunit_colors`" -ge 8 ]; then + _shunit_color_=${SHUNIT_TRUE} + fi + ;; + 'never'|'none') ;; # Support 'none' to support legacy usage. + *) _shunit_fatal "unrecognized color option '$1'" ;; + esac + + # shellcheck disable=SC2254 + case ${_shunit_color_} in + ${SHUNIT_TRUE}) + __shunit_ansi_none=${__SHUNIT_ANSI_NONE} + __shunit_ansi_red=${__SHUNIT_ANSI_RED} + __shunit_ansi_green=${__SHUNIT_ANSI_GREEN} + __shunit_ansi_yellow=${__SHUNIT_ANSI_YELLOW} + __shunit_ansi_cyan=${__SHUNIT_ANSI_CYAN} + ;; + ${SHUNIT_FALSE}) + __shunit_ansi_none='' + __shunit_ansi_red='' + __shunit_ansi_green='' + __shunit_ansi_yellow='' + __shunit_ansi_cyan='' + ;; + esac + + unset _shunit_color_ _shunit_tput_ +} + +# colors returns the number of supported colors for the TERM. +_shunit_colors() { + if _shunit_tput_=`${SHUNIT_CMD_TPUT} colors 2>/dev/null`; then + echo "${_shunit_tput_}" + else + echo 16 + fi + unset _shunit_tput_ +} + +# The actual running of the tests happens here. +# +# Args: +# None +_shunit_execSuite() { + for _shunit_test_ in ${__shunit_suite}; do + __shunit_testSuccess=${SHUNIT_TRUE} + + # Reset per-test info + __shunit_assertsCurrentTest=0 + __shunit_junitXmlCurrentTestCaseErrors='' + + # Disable skipping. + endSkipping + + # Execute the per-test setUp() function. + if ! setUp; then + _shunit_fatal "setUp() returned non-zero return code." + fi + + # Execute the test. + echo "${__SHUNIT_TEST_PREFIX}${_shunit_test_}" + # shellcheck disable=SC2086 + if ! eval ${_shunit_test_}; then + _shunit_error "${_shunit_test_}() returned non-zero return code." + __shunit_testSuccess=${SHUNIT_ERROR} + fi + + # Execute the per-test tearDown() function. + if ! tearDown; then + _shunit_fatal "tearDown() returned non-zero return code." + fi + + # Store current test case info in JUnit XML. + __shunit_junitXmlTestCases="${__shunit_junitXmlTestCases} + ${__shunit_junitXmlCurrentTestCaseErrors} + " + + # Update stats. + if ${__SHUNIT_BUILTIN} [ ${__shunit_testSuccess} -eq ${SHUNIT_TRUE} ]; then + __shunit_testsPassed=`expr "${__shunit_testsPassed}" + 1` + else + __shunit_testsFailed=`expr "${__shunit_testsFailed}" + 1` + fi + done + + unset _shunit_test_ +} + +# Generates the user friendly report with appropriate OK/FAILED message. +# +# Args: +# None +# Output: +# string: the report of successful and failed tests, as well as totals. +_shunit_generateReport() { + if ${__SHUNIT_BUILTIN} [ "${__shunit_reportGenerated}" -eq ${SHUNIT_TRUE} ]; then + return + fi + + _shunit_ok_=${SHUNIT_TRUE} + + # If no exit code was provided, determine an appropriate one. + if ${__SHUNIT_BUILTIN} [ "${__shunit_testsFailed}" -gt 0 -o ${__shunit_testSuccess} -eq ${SHUNIT_FALSE} ]; then + _shunit_ok_=${SHUNIT_FALSE} + fi + + echo + _shunit_msg_="Ran ${__shunit_ansi_cyan}${__shunit_testsTotal}${__shunit_ansi_none}" + if ${__SHUNIT_BUILTIN} [ "${__shunit_testsTotal}" -eq 1 ]; then + ${__SHUNIT_CMD_ECHO_ESC} "${_shunit_msg_} test." + else + ${__SHUNIT_CMD_ECHO_ESC} "${_shunit_msg_} tests." + fi + + if ${__SHUNIT_BUILTIN} [ -n "${__shunit_junitXmlOutputFile}" ]; then + echo " +${__shunit_junitXmlTestCases} +" > "${__shunit_junitXmlOutputFile}" + echo + echo "JUnit XML file ${__shunit_junitXmlOutputFile} was saved." + fi + + if ${__SHUNIT_BUILTIN} [ ${_shunit_ok_} -eq ${SHUNIT_TRUE} ]; then + _shunit_msg_="${__shunit_ansi_green}OK${__shunit_ansi_none}" + if ${__SHUNIT_BUILTIN} [ "${__shunit_assertsSkipped}" -gt 0 ]; then + _shunit_msg_="${_shunit_msg_} (${__shunit_ansi_yellow}skipped=${__shunit_assertsSkipped}${__shunit_ansi_none})" + fi + else + _shunit_msg_="${__shunit_ansi_red}FAILED${__shunit_ansi_none}" + _shunit_msg_="${_shunit_msg_} (${__shunit_ansi_red}failures=${__shunit_assertsFailed}${__shunit_ansi_none}" + if ${__SHUNIT_BUILTIN} [ "${__shunit_assertsSkipped}" -gt 0 ]; then + _shunit_msg_="${_shunit_msg_},${__shunit_ansi_yellow}skipped=${__shunit_assertsSkipped}${__shunit_ansi_none}" + fi + _shunit_msg_="${_shunit_msg_})" + fi + + echo + ${__SHUNIT_CMD_ECHO_ESC} "${_shunit_msg_}" + __shunit_reportGenerated=${SHUNIT_TRUE} + + unset _shunit_msg_ _shunit_ok_ +} + +# Test for whether a function should be skipped. +# +# Args: +# None +# Returns: +# boolean: whether the test should be skipped (TRUE/FALSE constant) +_shunit_shouldSkip() { + if ${__SHUNIT_BUILTIN} test ${__shunit_skip} -eq ${SHUNIT_FALSE}; then + return ${SHUNIT_FALSE} + fi + _shunit_assertSkip +} + +# Records a successful test. +# +# Args: +# None +_shunit_assertPass() { + __shunit_assertsPassed=`expr "${__shunit_assertsPassed}" + 1` + __shunit_assertsTotal=`expr "${__shunit_assertsTotal}" + 1` + __shunit_assertsCurrentTest=`expr "${__shunit_assertsCurrentTest}" + 1` +} + +# Records a test failure. +# +# Args: +# message: string: failure message to provide user +_shunit_assertFail() { + __shunit_testSuccess=${SHUNIT_FALSE} + _shunit_incFailedCount + + _shunit_xml_message_="`_shunit_escapeXmlData "$@"`" + + __shunit_junitXmlCurrentTestCaseErrors="${__shunit_junitXmlCurrentTestCaseErrors} + " + + if ${__SHUNIT_BUILTIN} [ $# -gt 0 ]; then + ${__SHUNIT_CMD_ECHO_ESC} "${__shunit_ansi_red}ASSERT:${__shunit_ansi_none}$*" + fi + + unset _shunit_xml_message_ +} + +# Increment the count of failed asserts. +# +# Args: +# none +_shunit_incFailedCount() { + __shunit_assertsFailed=`expr "${__shunit_assertsFailed}" + 1` + __shunit_assertsTotal=`expr "${__shunit_assertsTotal}" + 1` + __shunit_assertsCurrentTest=`expr "${__shunit_assertsCurrentTest}" + 1` +} + +# Records a skipped test. +# +# Args: +# None +_shunit_assertSkip() { + __shunit_assertsSkipped=`expr "${__shunit_assertsSkipped}" + 1` + __shunit_assertsTotal=`expr "${__shunit_assertsTotal}" + 1` + __shunit_assertsCurrentTest=`expr "${__shunit_assertsCurrentTest}" + 1` +} + +# Dump the current test metrics. +# +# Args: +# none +_shunit_metrics() { + echo "< \ +total: ${__shunit_assertsTotal} \ +passed: ${__shunit_assertsPassed} \ +failed: ${__shunit_assertsFailed} \ +skipped: ${__shunit_assertsSkipped} \ +>" +} + +# Prepare a script filename for sourcing. +# +# Args: +# script: string: path to a script to source +# Returns: +# string: filename prefixed with ./ (if necessary) +_shunit_prepForSourcing() { + _shunit_script_=$1 + case "${_shunit_script_}" in + /*|./*) echo "${_shunit_script_}" ;; + *) echo "./${_shunit_script_}" ;; + esac + unset _shunit_script_ +} + +# Extract list of functions to run tests against. +# +# Args: +# script: string: name of script to extract functions from +# Returns: +# string: of function names +_shunit_extractTestFunctions() { + _shunit_script_=$1 + + # Extract the lines with test function names, strip of anything besides the + # function name, and output everything on a single line. + _shunit_regex_='^\s*((function test[A-Za-z0-9_-]*)|(test[A-Za-z0-9_-]* *\(\)))' + grep -E "${_shunit_regex_}" "${_shunit_script_}" \ + |command sed 's/^[^A-Za-z0-9_-]*//;s/^function //;s/\([A-Za-z0-9_-]*\).*/\1/g' \ + |xargs + + unset _shunit_regex_ _shunit_script_ +} + +# Escape XML data. +# +# Args: +# data: string: data to escape +# Returns: +# string: escaped data +_shunit_escapeXmlData() { + # Required XML characters to escape are described here: + # http://www.w3.org/TR/REC-xml/#syntax + # https://www.liquid-technologies.com/Reference/Glossary/XML_EscapingData.html + echo "$*" \ + |command sed 's/&/\&/g;s//\>/g;s/"/\"/g'";s/'/\'/g" +} + +#------------------------------------------------------------------------------ +# Main. +# + +# Determine the operating mode. +if ${__SHUNIT_BUILTIN} [ $# -eq 0 -o "${1:-}" = '--' ]; then + __shunit_script=${__SHUNIT_PARENT} + __shunit_mode=${__SHUNIT_MODE_SOURCED} +else + __shunit_script=$1 + if ! ${__SHUNIT_BUILTIN} [ -r "${__shunit_script}" ]; then + _shunit_fatal "unable to read from ${__shunit_script}" + fi + __shunit_mode=${__SHUNIT_MODE_STANDALONE} +fi + +# Create a temporary storage location. +__shunit_tmpDir=`_shunit_mktempDir` + +# Provide a public temporary directory for unit test scripts. +# TODO(kward): document this. +SHUNIT_TMPDIR="${__shunit_tmpDir}/tmp" +if ! command mkdir "${SHUNIT_TMPDIR}"; then + _shunit_fatal "error creating SHUNIT_TMPDIR '${SHUNIT_TMPDIR}'" +fi + +# Configure traps to clean up after ourselves. +trap '_shunit_cleanup EXIT' 0 +trap '_shunit_cleanup INT' 2 +trap '_shunit_cleanup TERM' 15 + +# Create phantom functions to work around issues with Cygwin. +_shunit_mktempFunc +PATH="${__shunit_tmpDir}:${PATH}" + +# Make sure phantom functions are executable. This will bite if `/tmp` (or the +# current `$TMPDIR`) points to a path on a partition that was mounted with the +# 'noexec' option. The noexec command was created with `_shunit_mktempFunc()`. +noexec 2>/dev/null || _shunit_fatal \ + 'Please declare TMPDIR with path on partition with exec permission.' + +# We must manually source the tests in standalone mode. +if ${__SHUNIT_BUILTIN} [ "${__shunit_mode}" = "${__SHUNIT_MODE_STANDALONE}" ]; then + # shellcheck disable=SC1090 + ${__SHUNIT_BUILTIN} . "`_shunit_prepForSourcing \"${__shunit_script}\"`" +fi + +# Configure default output coloring behavior. +_shunit_configureColor "${SHUNIT_COLOR}" + +# Execute the oneTimeSetUp function (if it exists). +if ! oneTimeSetUp; then + _shunit_fatal "oneTimeSetUp() returned non-zero return code." +fi + +# Command line selected tests or suite selected tests +if ${__SHUNIT_BUILTIN} [ "$#" -ge 2 ]; then + # Argument $1 is either the filename of tests or '--'; either way, skip it. + shift + # Remaining arguments ($2 .. $#) are assumed to be: + # - test function names. + # - configuration options, that is started with the `--` prefix. + # Interate through all remaining args in "$@" in a POSIX (likely portable) way. + # Helpful tip: https://unix.stackexchange.com/questions/314032/how-to-use-arguments-like-1-2-in-a-for-loop + for _shunit_arg_ do + case "${_shunit_arg_}" in + --output-junit-xml=*) + # It is a request for JUnit XML output. + __shunit_junitXmlOutputFile="${_shunit_arg_#--output-junit-xml=}" + ;; + --suite-name=*) + # It is a request for a custom suite name. + __shunit_suiteName="${_shunit_arg_#--suite-name=}" + ;; + --*) + _shunit_fatal "unrecognized option \"${_shunit_arg_}\"" + ;; + *) + # It is the test name, process it in a usual way. + suite_addTest "${_shunit_arg_}" + ;; + esac + done + unset _shunit_arg_ +else + # Execute the suite function defined in the parent test script. + # DEPRECATED as of 2.1.0. + suite +fi + +# If no tests or suite specified, dynamically build a list of functions. +if ${__SHUNIT_BUILTIN} [ -z "${__shunit_suite}" ]; then + shunit_funcs_=`_shunit_extractTestFunctions "${__shunit_script}"` + for shunit_func_ in ${shunit_funcs_}; do + suite_addTest "${shunit_func_}" + done +fi +unset shunit_func_ shunit_funcs_ + +# If suite name is not defined, dynamically generate it from the script name. +if ${__SHUNIT_BUILTIN} [ -z "${__shunit_suiteName}" ]; then + __shunit_suiteName="${__shunit_script##*/}" +fi + +# Prepare the suite name for XML output. +__shunit_xmlSuiteName="`_shunit_escapeXmlData "${__shunit_suiteName}"`" + +# Execute the suite of unit tests. +_shunit_execSuite + +# Execute the oneTimeTearDown function (if it exists). +if ! oneTimeTearDown; then + _shunit_fatal "oneTimeTearDown() returned non-zero return code." +fi + +# Generate a report summary. +_shunit_generateReport + +# That's it folks. +if ! ${__SHUNIT_BUILTIN} [ "${__shunit_testsFailed}" -eq 0 ]; then + return ${SHUNIT_FALSE} +fi diff --git a/src/errors.rs b/src/errors.rs index 9198b43..9b3ae24 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -8,11 +8,11 @@ use snafu::prelude::*; pub enum SyntaxError { /// The first command in a path is not moveto. #[snafu(display( - "Invalid SVG path command '{command}' at index {index}, expected 'M' or 'm'" + "Invalid SVG path command '{character}' at index {index}, expected 'M' or 'm'" ))] ExpectedMovetoCommand { /// Command letter found - command: char, + character: char, /// Index of the command in the path index: usize, }, @@ -38,7 +38,7 @@ pub enum SyntaxError { /// Index of the character in the path index: usize, /// Expected character - expected: String, + expected: &'static str, }, /// Invalid path ending. @@ -47,7 +47,7 @@ pub enum SyntaxError { /// Index of the end of the path index: usize, /// Expected token - expected: String, + expected: &'static str, }, /// Invalid SVG quaractic arc command flag argument. diff --git a/src/lib.rs b/src/lib.rs index 2d15fd7..52cb97a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,11 +1,12 @@ #![doc = include_str!("../README.md")] #![warn(missing_docs)] #![doc(test(attr(deny(warnings))))] -#![no_std] +#![cfg_attr(not(test), no_std)] extern crate alloc; use crate::alloc::{ string::{String, ToString}, + vec, vec::Vec, }; @@ -13,7 +14,7 @@ use crate::alloc::{ ::doc_comment::doctest!("../README.md"); #[cfg(test)] -mod tests; +pub(crate) mod tests; mod errors; pub use errors::SyntaxError; @@ -142,6 +143,8 @@ impl SVGPathCommand { /// according to the SVG Path v1.1 specification. /// /// ``` +/// # // Don't test this in strict mode because it would fail +/// # #[cfg(not(feature = "strict"))] { /// use svg_path_cst::{svg_path_cst, SVGPathCSTNode, WSP}; /// /// let cst = svg_path_cst(b" \t\n\r \x0C"); @@ -189,6 +192,7 @@ impl SVGPathCommand { /// _ => (), /// } /// } +/// # } /// ``` #[derive(Debug, PartialEq, Clone, Copy)] #[repr(u8)] @@ -461,17 +465,20 @@ impl<'a> Parser<'a> { Some(self.path[self.index]) } - fn check_unexpected_end(&mut self, expected: &str) -> Result<(), SyntaxError> { + fn check_unexpected_end( + &mut self, + expected: &'static str, + ) -> Result<(), SyntaxError> { if self.peek().is_none() { return Err(SyntaxError::UnexpectedEnding { index: self.index - 1, - expected: expected.to_string(), + expected, }); } Ok(()) } - #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] + #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip_all))] fn parse_whitespaces(&mut self, nodes: &mut Vec) { while let Some(next) = self.peek() { match next { @@ -522,7 +529,7 @@ impl<'a> Parser<'a> { #[cfg_attr( feature = "tracing", - tracing::instrument(level = "trace", skip(self), err(Debug)) + tracing::instrument(level = "trace", skip_all, err(Debug)) )] fn parse_comma_wsp( &mut self, @@ -546,7 +553,7 @@ impl<'a> Parser<'a> { } else { return Err(SyntaxError::UnexpectedEnding { index: self.index - 1, - expected: "comma or whitespace".to_string(), + expected: "comma or whitespace", }); } @@ -555,7 +562,7 @@ impl<'a> Parser<'a> { #[cfg_attr( feature = "tracing", - tracing::instrument(level = "trace", skip(self), err(Debug)) + tracing::instrument(level = "trace", skip_all, err(Debug)) )] fn parse_number(&mut self) -> Result { let start = self.index; @@ -615,7 +622,7 @@ impl<'a> Parser<'a> { return Err(SyntaxError::InvalidCharacter { character: next as char, index: self.index - 1, - expected: "number or command".to_string(), + expected: "number or command", }); } else if !has_digit { number.push(next); @@ -654,7 +661,7 @@ impl<'a> Parser<'a> { } } - #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] + #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip_all))] fn parse_sign(&mut self) -> Option { match self.peek() { Some(b'+') => { @@ -677,7 +684,7 @@ impl<'a> Parser<'a> { #[cfg_attr( feature = "tracing", - tracing::instrument(level = "trace", skip(self), err(Debug)) + tracing::instrument(level = "trace", skip_all, err(Debug)) )] fn parse_flag(&mut self, command: u8) -> Result { match self.next() { @@ -690,14 +697,14 @@ impl<'a> Parser<'a> { }), None => Err(SyntaxError::UnexpectedEnding { index: self.index, - expected: "flag (0 or 1)".to_string(), + expected: "flag (0 or 1)", }), } } #[cfg_attr( feature = "tracing", - tracing::instrument(level = "trace", skip(self), err(Debug)) + tracing::instrument(level = "trace", skip_all, err(Debug)) )] fn parse_coordinate(&mut self) -> Result { let sign_node = self.parse_sign(); @@ -710,7 +717,7 @@ impl<'a> Parser<'a> { else { Err(SyntaxError::UnexpectedEnding { index: self.index - 1, - expected: "number".to_string(), + expected: "number", })? }; @@ -737,7 +744,7 @@ impl<'a> Parser<'a> { #[cfg_attr( feature = "tracing", - tracing::instrument(level = "trace", skip(self), err(Debug)) + tracing::instrument(level = "trace", skip_all, err(Debug)) )] fn parse_coordinate_pair( &mut self, @@ -763,7 +770,7 @@ impl<'a> Parser<'a> { #[cfg_attr( feature = "tracing", - tracing::instrument(level = "trace", skip(self), err(Debug)) + tracing::instrument(level = "trace", skip_all, err(Debug)) )] fn parse_two_operands_command( &mut self, @@ -808,7 +815,7 @@ impl<'a> Parser<'a> { #[cfg_attr( feature = "tracing", - tracing::instrument(level = "trace", skip(self), err(Debug)) + tracing::instrument(level = "trace", skip_all, err(Debug)) )] fn parse_four_operands_command( &mut self, @@ -861,7 +868,7 @@ impl<'a> Parser<'a> { #[cfg_attr( feature = "tracing", - tracing::instrument(level = "trace", skip(self), err(Debug)) + tracing::instrument(level = "trace", skip_all, err(Debug)) )] fn parse_curveto( &mut self, @@ -924,7 +931,7 @@ impl<'a> Parser<'a> { #[cfg_attr( feature = "tracing", - tracing::instrument(level = "trace", skip(self), err(Debug)) + tracing::instrument(level = "trace", skip_all, err(Debug)) )] fn parse_arc( &mut self, @@ -1086,18 +1093,25 @@ impl<'a> Parser<'a> { Ok(cst) } - #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] + #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip_all))] fn parse_closepath(&mut self, command: &'static SVGPathCommand) -> SVGPathCSTNode { - let mut segment = new_segment(command, self.index - 1, false); - segment.end = self.index; - segment.chain_end = self.index; - segment.cst.push(SVGPathCSTNode::Command(command)); - SVGPathCSTNode::Segment(segment) + let start = self.index - 1; + let end = self.index; + SVGPathCSTNode::Segment(SVGPathSegment { + command, + args: Vec::with_capacity(0), + cst: vec![SVGPathCSTNode::Command(command); 1], + start, + end, + chained: false, + chain_start: start, + chain_end: end, + }) } #[cfg_attr( feature = "tracing", - tracing::instrument(level = "trace", skip(self), err(Debug)) + tracing::instrument(level = "trace", skip_all, err(Debug)) )] fn parse_horizontal_or_vertical( &mut self, @@ -1152,179 +1166,177 @@ impl<'a> Parser<'a> { #[cfg_attr( feature = "tracing", - tracing::instrument(level = "trace", skip(self), err(Debug)) - )] - fn parse_drawto( - &mut self, - command: u8, - nodes: &mut Vec, - ) -> Result<(), SyntaxError> { - match command { - b'm' => { - nodes.extend( - self.parse_two_operands_command(&SVGPathCommand::MovetoLower)?, - ); - Ok(()) - } - b'M' => { - nodes.extend( - self.parse_two_operands_command(&SVGPathCommand::MovetoUpper)?, - ); - Ok(()) - } - b'l' => { - nodes.extend( - self.parse_two_operands_command(&SVGPathCommand::LinetoLower)?, - ); - Ok(()) - } - b'L' => { - nodes.extend( - self.parse_two_operands_command(&SVGPathCommand::LinetoUpper)?, - ); - Ok(()) - } - b'h' => { - nodes.extend( - self.parse_horizontal_or_vertical( - &SVGPathCommand::HorizontalLower, - )?, - ); - Ok(()) - } - b'H' => { - nodes.extend( - self.parse_horizontal_or_vertical( - &SVGPathCommand::HorizontalUpper, - )?, - ); - Ok(()) - } - b'v' => { - nodes.extend( - self.parse_horizontal_or_vertical(&SVGPathCommand::VerticalLower)?, - ); - Ok(()) - } - b'V' => { - nodes.extend( - self.parse_horizontal_or_vertical(&SVGPathCommand::VerticalUpper)?, - ); - Ok(()) - } - b'z' => { - nodes.push(self.parse_closepath(&SVGPathCommand::ClosepathLower)); - Ok(()) - } - b'Z' => { - nodes.push(self.parse_closepath(&SVGPathCommand::ClosepathUpper)); - Ok(()) - } - b'c' => { - nodes.extend(self.parse_curveto(&SVGPathCommand::CurvetoLower)?); - Ok(()) - } - b'C' => { - nodes.extend(self.parse_curveto(&SVGPathCommand::CurvetoUpper)?); - Ok(()) - } - b'q' => { - nodes.extend( - self.parse_four_operands_command(&SVGPathCommand::QuadraticLower)?, - ); - Ok(()) - } - b'Q' => { - nodes.extend( - self.parse_four_operands_command(&SVGPathCommand::QuadraticUpper)?, - ); - Ok(()) - } - b's' => { - nodes.extend(self.parse_four_operands_command( - &SVGPathCommand::SmoothCurvetoLower, - )?); - Ok(()) - } - b'S' => { - nodes.extend(self.parse_four_operands_command( - &SVGPathCommand::SmoothCurvetoUpper, - )?); - Ok(()) - } - b'a' => { - nodes.extend(self.parse_arc(&SVGPathCommand::ArcLower)?); - Ok(()) - } - b'A' => { - nodes.extend(self.parse_arc(&SVGPathCommand::ArcUpper)?); - Ok(()) - } - b't' => { - nodes.extend(self.parse_two_operands_command( - &SVGPathCommand::SmoothQuadraticLower, - )?); - Ok(()) - } - b'T' => { - nodes.extend(self.parse_two_operands_command( - &SVGPathCommand::SmoothQuadraticUpper, - )?); - Ok(()) - } - _ => Err(SyntaxError::InvalidCharacter { - character: command as char, - index: self.index - 1, - expected: "command".to_string(), - }), - } - } - - #[cfg_attr( - feature = "tracing", - tracing::instrument(level = "trace", skip(self), err(Debug)) + tracing::instrument(level = "trace", skip_all, err(Debug)) )] pub fn parse(&mut self) -> Result, SyntaxError> { if self.path.is_empty() { #[cfg(feature = "tracing")] tracing::trace!("Empty SVG path"); + + #[cfg(feature = "strict")] + return Err(SyntaxError::UnexpectedEnding { + expected: "moveto command", + index: 0, + }); + + #[cfg(not(feature = "strict"))] return Ok(Vec::new()); } + if self.path == b"none" { #[cfg(feature = "tracing")] tracing::trace!("SVG path with 'none' value"); + + #[cfg(feature = "strict")] + return Err(SyntaxError::ExpectedMovetoCommand { + character: 'n', + index: 0, + }); + + #[cfg(not(feature = "strict"))] return Ok(Vec::from([SVGPathCSTNode::None])); } - let mut cst = Vec::new(); - self.parse_whitespaces(&mut cst); + let mut nodes = Vec::with_capacity(self.path.len() / 4); + self.parse_whitespaces(&mut nodes); let next = self.next().unwrap_or(b' '); match next { b'm' | b'M' => { - for node in self.parse_two_operands_command(match next { + nodes.extend(self.parse_two_operands_command(match next { b'm' => &SVGPathCommand::MovetoLower, _ => &SVGPathCommand::MovetoUpper, - })? { - cst.push(node); - } - self.parse_whitespaces(&mut cst); + })?); + self.parse_whitespaces(&mut nodes); while let Some(next) = self.next() { - self.parse_drawto(next, &mut cst)?; - self.parse_whitespaces(&mut cst); + match next { + b'm' => { + nodes.extend(self.parse_two_operands_command( + &SVGPathCommand::MovetoLower, + )?); + } + b'M' => { + nodes.extend(self.parse_two_operands_command( + &SVGPathCommand::MovetoUpper, + )?); + } + b'l' => { + nodes.extend(self.parse_two_operands_command( + &SVGPathCommand::LinetoLower, + )?); + } + b'L' => { + nodes.extend(self.parse_two_operands_command( + &SVGPathCommand::LinetoUpper, + )?); + } + b'h' => { + nodes.extend(self.parse_horizontal_or_vertical( + &SVGPathCommand::HorizontalLower, + )?); + } + b'H' => { + nodes.extend(self.parse_horizontal_or_vertical( + &SVGPathCommand::HorizontalUpper, + )?); + } + b'v' => { + nodes.extend(self.parse_horizontal_or_vertical( + &SVGPathCommand::VerticalLower, + )?); + } + b'V' => { + nodes.extend(self.parse_horizontal_or_vertical( + &SVGPathCommand::VerticalUpper, + )?); + } + b'z' => { + nodes.push( + self.parse_closepath(&SVGPathCommand::ClosepathLower), + ); + } + b'Z' => { + nodes.push( + self.parse_closepath(&SVGPathCommand::ClosepathUpper), + ); + } + b'c' => { + nodes.extend( + self.parse_curveto(&SVGPathCommand::CurvetoLower)?, + ); + } + b'C' => { + nodes.extend( + self.parse_curveto(&SVGPathCommand::CurvetoUpper)?, + ); + } + b'q' => { + nodes.extend(self.parse_four_operands_command( + &SVGPathCommand::QuadraticLower, + )?); + } + b'Q' => { + nodes.extend(self.parse_four_operands_command( + &SVGPathCommand::QuadraticUpper, + )?); + } + b's' => { + nodes.extend(self.parse_four_operands_command( + &SVGPathCommand::SmoothCurvetoLower, + )?); + } + b'S' => { + nodes.extend(self.parse_four_operands_command( + &SVGPathCommand::SmoothCurvetoUpper, + )?); + } + b'a' => { + nodes.extend(self.parse_arc(&SVGPathCommand::ArcLower)?); + } + b'A' => { + nodes.extend(self.parse_arc(&SVGPathCommand::ArcUpper)?); + } + b't' => { + nodes.extend(self.parse_two_operands_command( + &SVGPathCommand::SmoothQuadraticLower, + )?); + } + b'T' => { + nodes.extend(self.parse_two_operands_command( + &SVGPathCommand::SmoothQuadraticUpper, + )?); + } + _ => { + return Err(SyntaxError::InvalidCharacter { + character: next as char, + index: self.index - 1, + expected: "command", + }) + } + } + self.parse_whitespaces(&mut nodes); } } - b' ' => (), + b' ' => { + #[cfg(all(feature = "tracing", feature = "strict"))] + tracing::trace!("Empty SVG path"); + + #[cfg(feature = "strict")] + return Err(SyntaxError::ExpectedMovetoCommand { + character: self.path[0] as char, + index: 0, + }); + } _ => { #[cfg(feature = "tracing")] tracing::trace!("Expected moveto command, found '{}'", next as char); return Err(SyntaxError::ExpectedMovetoCommand { - command: next as char, + character: next as char, index: self.index - 1, }); } } - Ok(cst) + Ok(nodes) } } @@ -1343,7 +1355,7 @@ impl<'a> Parser<'a> { /// Err(SVGPathSyntaxError::InvalidCharacter { /// character: '!', /// index: 6, -/// expected: "number or command".to_string(), +/// expected: "number or command", /// }) /// ); /// @@ -1355,7 +1367,7 @@ impl<'a> Parser<'a> { #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip_all))] pub fn svg_path_cst(path: &[u8]) -> Result, SyntaxError> { #[cfg(feature = "tracing")] - tracing::info!("Parsing SVG path: {:?}", path); + tracing::trace!("{:?}", &path.iter().map(|&c| c as char).collect::()); let mut parser = Parser::new(path); parser.parse() } diff --git a/src/main.rs b/src/main.rs index 89579cf..f97933e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,8 +2,16 @@ use svg_path_cst::svg_path_cst; fn main() { let args: Vec = std::env::args().collect(); - let data = args[args.len() - 1].as_bytes(); - match svg_path_cst(data) { + if args.contains(&String::from("--help")) + || args.contains(&String::from("-h")) + || args.len() < 2 + { + eprintln!("Usage: svg-path-cst \"\""); + std::process::exit(1); + } + + let svg_path = args[args.len() - 1].as_bytes(); + match svg_path_cst(svg_path) { Ok(result) => { #[allow(clippy::print_stdout)] { @@ -11,7 +19,7 @@ fn main() { } } Err(e) => { - eprintln!("{e:?}"); + eprintln!("Error: {e}"); std::process::exit(1); } } diff --git a/src/tests/helpers.rs b/src/tests/helpers.rs new file mode 100644 index 0000000..99631c8 --- /dev/null +++ b/src/tests/helpers.rs @@ -0,0 +1,17 @@ +use crate::{svg_path_cst, SyntaxError}; + +#[cfg_attr(feature = "strict", allow(dead_code))] +pub(crate) fn assert_svg_path_cst(path: &[u8], expected: Vec) { + assert_eq!(svg_path_cst(path), Ok(expected)); +} + +#[cfg_attr(feature = "strict", allow(dead_code))] +pub(crate) fn assert_svg_path_cst_fmt(path: &[u8], expected: &str) { + let cst = svg_path_cst(path); + assert!(cst.is_ok()); + assert_eq!(format!("{:?}", cst.unwrap()), expected); +} + +pub(crate) fn assert_svg_path_cst_err(path: &[u8], expected: SyntaxError) { + assert_eq!(svg_path_cst(path), Err(expected)); +} diff --git a/src/tests/integration.sh b/src/tests/integration.sh new file mode 100644 index 0000000..ae42658 --- /dev/null +++ b/src/tests/integration.sh @@ -0,0 +1,64 @@ +#!/bin/sh + +: ' + Integration tests for svg-path-cst binary. + + Environment variables: + + - DEBUG: If not empty, all executed commands will be shown in the output. +' + +testSimpleicons() { + output="$(./target/debug/svg-path-cst "$(cat fuzz/corpus/simpleicons.txt)")" + assertContains "$output" "[Segment(SVGPathSegment { command: MovetoUpper, args: [12.0, 0.0]" + assertContains "$output" "chain_end: 449 })]" +} + +testElsevier() { + output="$(./target/debug/svg-path-cst "$(cat fuzz/corpus/elsevier.txt)")" + assertContains "$output" "[Segment(SVGPathSegment { command: MovetoUpper" + assertContains "$output" "chain_end: 80118 })]" +} + +testFailsOnInvalidInput() { + output="$(./target/debug/svg-path-cst "invalid" 2>&1)" + exitCode=$? + assertEquals "$exitCode" 1 + assertContains "$output" "Error: Invalid SVG path command 'i' at index 0, expected 'M' or 'm'" +} + +testHelp() { + output="$(./target/debug/svg-path-cst --help 2>&1)" + exitCode=$? + assertEquals "$exitCode" 1 + assertContains "$output" "Usage: svg-path-cst \"\"" +} + +testNoArgs() { + output="$(./target/debug/svg-path-cst 2>&1)" + exitCode=$? + assertEquals "$exitCode" 1 + assertContains "$output" "Usage: svg-path-cst \"\"" +} + +testAdditionalArgsAreIgnored() { + output="$(./target/debug/svg-path-cst foo bar baz "$(cat fuzz/corpus/simpleicons.txt)" 2>&1)" + assertContains "$output" "[Segment(SVGPathSegment { command: MovetoUpper, args: [12.0, 0.0]" + assertContains "$output" "chain_end: 449 })]" +} + +prepare() { + cargo build + + set -e + if [ -n "$DEBUG" ]; then + set -x + fi + + if [ ! -f "shunit2" ]; then + curl -sSL https://raw.githubusercontent.com/kward/shunit2/master/shunit2 \ + -o shunit2 + fi +} + +prepare && . ./shunit2 diff --git a/src/tests/mod.rs b/src/tests/mod.rs new file mode 100644 index 0000000..a03e89c --- /dev/null +++ b/src/tests/mod.rs @@ -0,0 +1,7 @@ +pub(in crate::tests) mod helpers; + +#[cfg(not(feature = "strict"))] +mod no_features; + +#[cfg(feature = "strict")] +mod strict; diff --git a/src/tests.rs b/src/tests/no_features.rs similarity index 85% rename from src/tests.rs rename to src/tests/no_features.rs index d8d9ff7..1d7c4ed 100644 --- a/src/tests.rs +++ b/src/tests/no_features.rs @@ -1,47 +1,27 @@ -extern crate alloc; -use crate::alloc::{format, string::ToString, vec, vec::Vec}; - -use crate::errors::SyntaxError; -use crate::{svg_path_cst, SVGPathCSTNode, SVGPathCommand, SVGPathSegment, Sign, WSP}; - -macro_rules! assert_svg_path_cst { - ($path:expr, $expected:expr) => { - assert_eq!(svg_path_cst($path), Ok($expected)); - }; -} - -macro_rules! assert_svg_path_cst_fmt { - ($path:expr, $expected:expr) => { - let cst = svg_path_cst($path); - assert!(cst.is_ok()); - assert_eq!(format!("{:?}", cst.unwrap()), $expected); - }; -} - -macro_rules! assert_svg_path_cst_err { - ($path:expr, $expected:expr) => { - assert_eq!(svg_path_cst($path), Err($expected)); - }; -} +use crate::tests::helpers::*; +use crate::{ + svg_path_cst, SVGPathCSTNode, SVGPathCommand, SVGPathSegment, Sign, SyntaxError, + WSP, +}; #[test] -fn test_empty() { - assert_svg_path_cst!(b"", Vec::with_capacity(0)); +fn empty() { + assert_svg_path_cst(b"", Vec::new()); } #[test] -fn test_none() { - assert_svg_path_cst!(b"none", vec![SVGPathCSTNode::None]); +fn none() { + assert_svg_path_cst(b"none", vec![SVGPathCSTNode::None]); } #[test] -fn test_none_fmt() { - assert_svg_path_cst_fmt!(b"none", "[None]"); +fn none_fmt() { + assert_svg_path_cst_fmt(b"none", "[None]"); } #[test] -fn test_whitespaces() { - assert_svg_path_cst!( +fn whitespaces() { + assert_svg_path_cst( b" \t\n\r \x0C", vec![ SVGPathCSTNode::Whitespace { @@ -74,13 +54,13 @@ fn test_whitespaces() { start: 5, end: 6, }, - ] + ], ); } #[test] -fn test_whitespaces_fmt() { - assert_svg_path_cst_fmt!( +fn whitespaces_fmt() { + assert_svg_path_cst_fmt( b" \t\n\r \x0C", concat!( "[Whitespace { wsp: Space, start: 0, end: 1 },", @@ -89,92 +69,92 @@ fn test_whitespaces_fmt() { " Whitespace { wsp: CarriageReturn, start: 3, end: 4 },", " Whitespace { wsp: Space, start: 4, end: 5 },", " Whitespace { wsp: FormFeed, start: 5, end: 6 }]", - ) + ), ); } #[test] fn invalid_character() { - let expected = "number or command".to_string(); + let expected = "number or command"; - assert_svg_path_cst_err!( + assert_svg_path_cst_err( b"m0 0 !l10 10", SyntaxError::InvalidCharacter { character: '!', index: 5, - expected: expected.clone(), - } + expected, + }, ); - assert_svg_path_cst_err!( + assert_svg_path_cst_err( b"m0 0 l!10 10", SyntaxError::InvalidCharacter { character: '!', index: 6, - expected: expected.clone(), - } + expected, + }, ); - assert_svg_path_cst_err!( + assert_svg_path_cst_err( b"m0 ! 0", SyntaxError::InvalidCharacter { character: '!', index: 3, - expected: expected.clone(), - } + expected, + }, ); - assert_svg_path_cst_err!( + assert_svg_path_cst_err( b"m0 \t\n 0!", SyntaxError::InvalidCharacter { character: '!', index: 7, - expected: expected.clone(), - } + expected, + }, ); } #[test] fn invalid_moveto_at_start() { - assert_svg_path_cst_err!( + assert_svg_path_cst_err( b"A 10 10", SyntaxError::ExpectedMovetoCommand { - command: 'A', + character: 'A', index: 0, - } + }, ); } #[test] fn invalid_moveto_after_wsp() { - assert_svg_path_cst_err!( + assert_svg_path_cst_err( b" \t\n\r \x0C A 10 10", SyntaxError::ExpectedMovetoCommand { - command: 'A', + character: 'A', index: 7, - } + }, ); } #[test] fn invalid_end_in_moveto() { - assert_svg_path_cst_err!( + assert_svg_path_cst_err( b"m 10", SyntaxError::UnexpectedEnding { index: 3, - expected: "comma or whitespace".to_string(), - } + expected: "comma or whitespace", + }, ); } #[test] fn invalid_moveto_args() { - assert_svg_path_cst_err!( + assert_svg_path_cst_err( b"m 1 2 3", SyntaxError::UnexpectedEnding { index: 6, - expected: "comma or whitespace".to_string(), - } + expected: "comma or whitespace", + }, ); } @@ -194,7 +174,7 @@ fn basic_moveto() { end: 2, }, SVGPathCSTNode::Number { - raw_number: "10".to_string(), + raw_number: "10".into(), value: 10.0, start: 2, end: 4, @@ -204,7 +184,7 @@ fn basic_moveto() { start: 4, }, SVGPathCSTNode::Number { - raw_number: "10".to_string(), + raw_number: "10".into(), value: 10.0, start: 5, end: 7, @@ -221,7 +201,7 @@ fn basic_moveto() { #[test] fn basic_moveto_fmt() { - assert_svg_path_cst_fmt!( + assert_svg_path_cst_fmt( b"m 10-10", concat!( "[Segment(SVGPathSegment {", @@ -236,13 +216,13 @@ fn basic_moveto_fmt() { " start: 0, end: 7, chained: false,", " chain_start: 0, chain_end: 7", " })]", - ) + ), ); } #[test] fn moveto_whitespaces() { - assert_svg_path_cst!( + assert_svg_path_cst( b" M \t10\r 10 ", vec![ SVGPathCSTNode::Whitespace { @@ -266,7 +246,7 @@ fn moveto_whitespaces() { end: 4, }, SVGPathCSTNode::Number { - raw_number: "10".to_string(), + raw_number: "10".into(), value: 10.0, start: 4, end: 6, @@ -282,7 +262,7 @@ fn moveto_whitespaces() { end: 8, }, SVGPathCSTNode::Number { - raw_number: "10".to_string(), + raw_number: "10".into(), value: 10.0, start: 8, end: 10, @@ -299,13 +279,13 @@ fn moveto_whitespaces() { start: 10, end: 11, }, - ] + ], ); } #[test] fn chained_moveto() { - assert_svg_path_cst!( + assert_svg_path_cst( b"M 10 10 20 20", vec![ SVGPathCSTNode::Segment(SVGPathSegment { @@ -319,7 +299,7 @@ fn chained_moveto() { end: 2, }, SVGPathCSTNode::Number { - raw_number: "10".to_string(), + raw_number: "10".into(), value: 10.0, start: 2, end: 4, @@ -330,7 +310,7 @@ fn chained_moveto() { end: 5, }, SVGPathCSTNode::Number { - raw_number: "10".to_string(), + raw_number: "10".into(), value: 10.0, start: 5, end: 7, @@ -352,7 +332,7 @@ fn chained_moveto() { args: vec![20.0, 20.0], cst: vec![ SVGPathCSTNode::Number { - raw_number: "20".to_string(), + raw_number: "20".into(), value: 20.0, start: 8, end: 10, @@ -363,7 +343,7 @@ fn chained_moveto() { end: 11, }, SVGPathCSTNode::Number { - raw_number: "20".to_string(), + raw_number: "20".into(), value: 20.0, start: 11, end: 13, @@ -375,13 +355,13 @@ fn chained_moveto() { chain_start: 0, chain_end: 13, }), - ] + ], ); } #[test] fn moveto_and_moveto_drawto() { - assert_svg_path_cst!( + assert_svg_path_cst( b"M10 10 M5 -4.6", vec![ SVGPathCSTNode::Segment(SVGPathSegment { @@ -390,7 +370,7 @@ fn moveto_and_moveto_drawto() { cst: vec![ SVGPathCSTNode::Command(&SVGPathCommand::MovetoUpper), SVGPathCSTNode::Number { - raw_number: "10".to_string(), + raw_number: "10".into(), value: 10.0, start: 1, end: 3, @@ -401,7 +381,7 @@ fn moveto_and_moveto_drawto() { end: 4, }, SVGPathCSTNode::Number { - raw_number: "10".to_string(), + raw_number: "10".into(), value: 10.0, start: 4, end: 6, @@ -424,7 +404,7 @@ fn moveto_and_moveto_drawto() { cst: vec![ SVGPathCSTNode::Command(&SVGPathCommand::MovetoUpper), SVGPathCSTNode::Number { - raw_number: "5".to_string(), + raw_number: "5".into(), value: 5.0, start: 8, end: 9, @@ -439,7 +419,7 @@ fn moveto_and_moveto_drawto() { start: 10, }, SVGPathCSTNode::Number { - raw_number: "4.6".to_string(), + raw_number: "4.6".into(), value: 4.6, start: 11, end: 14, @@ -451,13 +431,13 @@ fn moveto_and_moveto_drawto() { chain_start: 7, chain_end: 14, }), - ] + ], ); } #[test] fn horizontal_and_vertical() { - assert_svg_path_cst!( + assert_svg_path_cst( b"m1 2h6v-3 5", vec![ SVGPathCSTNode::Segment(SVGPathSegment { @@ -466,7 +446,7 @@ fn horizontal_and_vertical() { cst: vec![ SVGPathCSTNode::Command(&SVGPathCommand::MovetoLower), SVGPathCSTNode::Number { - raw_number: "1".to_string(), + raw_number: "1".into(), value: 1.0, start: 1, end: 2, @@ -477,7 +457,7 @@ fn horizontal_and_vertical() { end: 3, }, SVGPathCSTNode::Number { - raw_number: "2".to_string(), + raw_number: "2".into(), value: 2.0, start: 3, end: 4, @@ -495,7 +475,7 @@ fn horizontal_and_vertical() { cst: vec![ SVGPathCSTNode::Command(&SVGPathCommand::HorizontalLower), SVGPathCSTNode::Number { - raw_number: "6".to_string(), + raw_number: "6".into(), value: 6.0, start: 5, end: 6, @@ -517,7 +497,7 @@ fn horizontal_and_vertical() { start: 7, }, SVGPathCSTNode::Number { - raw_number: "3".to_string(), + raw_number: "3".into(), value: 3.0, start: 8, end: 9, @@ -538,24 +518,24 @@ fn horizontal_and_vertical() { command: &SVGPathCommand::VerticalLower, args: vec![5.0], cst: vec![SVGPathCSTNode::Number { - raw_number: "5".to_string(), + raw_number: "5".into(), value: 5.0, start: 10, end: 11, - },], + }], start: 10, end: 11, chained: true, chain_start: 6, chain_end: 11, }), - ] + ], ); } #[test] fn moveto_curveto() { - assert_svg_path_cst!( + assert_svg_path_cst( b"m0 0c100,100 250,100 250,200 200,200 500,200 500,400", vec![ SVGPathCSTNode::Segment(SVGPathSegment { @@ -564,7 +544,7 @@ fn moveto_curveto() { cst: vec![ SVGPathCSTNode::Command(&SVGPathCommand::MovetoLower), SVGPathCSTNode::Number { - raw_number: "0".to_string(), + raw_number: "0".into(), value: 0.0, start: 1, end: 2, @@ -575,7 +555,7 @@ fn moveto_curveto() { end: 3, }, SVGPathCSTNode::Number { - raw_number: "0".to_string(), + raw_number: "0".into(), value: 0.0, start: 3, end: 4, @@ -593,14 +573,14 @@ fn moveto_curveto() { cst: vec![ SVGPathCSTNode::Command(&SVGPathCommand::CurvetoLower), SVGPathCSTNode::Number { - raw_number: "100".to_string(), + raw_number: "100".into(), value: 100.0, start: 5, end: 8, }, SVGPathCSTNode::Comma { start: 8 }, SVGPathCSTNode::Number { - raw_number: "100".to_string(), + raw_number: "100".into(), value: 100.0, start: 9, end: 12, @@ -611,14 +591,14 @@ fn moveto_curveto() { end: 13, }, SVGPathCSTNode::Number { - raw_number: "250".to_string(), + raw_number: "250".into(), value: 250.0, start: 13, end: 16, }, SVGPathCSTNode::Comma { start: 16 }, SVGPathCSTNode::Number { - raw_number: "100".to_string(), + raw_number: "100".into(), value: 100.0, start: 17, end: 20, @@ -629,14 +609,14 @@ fn moveto_curveto() { end: 21, }, SVGPathCSTNode::Number { - raw_number: "250".to_string(), + raw_number: "250".into(), value: 250.0, start: 21, end: 24, }, SVGPathCSTNode::Comma { start: 24 }, SVGPathCSTNode::Number { - raw_number: "200".to_string(), + raw_number: "200".into(), value: 200.0, start: 25, end: 28, @@ -658,14 +638,14 @@ fn moveto_curveto() { args: vec![200.0, 200.0, 500.0, 200.0, 500.0, 400.0], cst: vec![ SVGPathCSTNode::Number { - raw_number: "200".to_string(), + raw_number: "200".into(), value: 200.0, start: 29, end: 32, }, SVGPathCSTNode::Comma { start: 32 }, SVGPathCSTNode::Number { - raw_number: "200".to_string(), + raw_number: "200".into(), value: 200.0, start: 33, end: 36, @@ -676,14 +656,14 @@ fn moveto_curveto() { end: 37, }, SVGPathCSTNode::Number { - raw_number: "500".to_string(), + raw_number: "500".into(), value: 500.0, start: 37, end: 40, }, SVGPathCSTNode::Comma { start: 40 }, SVGPathCSTNode::Number { - raw_number: "200".to_string(), + raw_number: "200".into(), value: 200.0, start: 41, end: 44, @@ -694,14 +674,14 @@ fn moveto_curveto() { end: 45, }, SVGPathCSTNode::Number { - raw_number: "500".to_string(), + raw_number: "500".into(), value: 500.0, start: 45, end: 48, }, SVGPathCSTNode::Comma { start: 48 }, SVGPathCSTNode::Number { - raw_number: "400".to_string(), + raw_number: "400".into(), value: 400.0, start: 49, end: 52, @@ -713,13 +693,13 @@ fn moveto_curveto() { chain_start: 4, chain_end: 52, }), - ] + ], ); } #[test] fn moveto_smooth_curveto() { - assert_svg_path_cst!( + assert_svg_path_cst( b"m0 0s100,100 250,200 150 150 300 300", vec![ SVGPathCSTNode::Segment(SVGPathSegment { @@ -728,7 +708,7 @@ fn moveto_smooth_curveto() { cst: vec![ SVGPathCSTNode::Command(&SVGPathCommand::MovetoLower), SVGPathCSTNode::Number { - raw_number: "0".to_string(), + raw_number: "0".into(), value: 0.0, start: 1, end: 2, @@ -739,7 +719,7 @@ fn moveto_smooth_curveto() { end: 3, }, SVGPathCSTNode::Number { - raw_number: "0".to_string(), + raw_number: "0".into(), value: 0.0, start: 3, end: 4, @@ -757,14 +737,14 @@ fn moveto_smooth_curveto() { cst: vec![ SVGPathCSTNode::Command(&SVGPathCommand::SmoothCurvetoLower), SVGPathCSTNode::Number { - raw_number: "100".to_string(), + raw_number: "100".into(), value: 100.0, start: 5, end: 8, }, SVGPathCSTNode::Comma { start: 8 }, SVGPathCSTNode::Number { - raw_number: "100".to_string(), + raw_number: "100".into(), value: 100.0, start: 9, end: 12, @@ -775,14 +755,14 @@ fn moveto_smooth_curveto() { end: 13, }, SVGPathCSTNode::Number { - raw_number: "250".to_string(), + raw_number: "250".into(), value: 250.0, start: 13, end: 16, }, SVGPathCSTNode::Comma { start: 16 }, SVGPathCSTNode::Number { - raw_number: "200".to_string(), + raw_number: "200".into(), value: 200.0, start: 17, end: 20, @@ -804,7 +784,7 @@ fn moveto_smooth_curveto() { args: vec![150.0, 150.0, 300.0, 300.0], cst: vec![ SVGPathCSTNode::Number { - raw_number: "150".to_string(), + raw_number: "150".into(), value: 150.0, start: 21, end: 24, @@ -815,7 +795,7 @@ fn moveto_smooth_curveto() { end: 25, }, SVGPathCSTNode::Number { - raw_number: "150".to_string(), + raw_number: "150".into(), value: 150.0, start: 25, end: 28, @@ -826,7 +806,7 @@ fn moveto_smooth_curveto() { end: 29, }, SVGPathCSTNode::Number { - raw_number: "300".to_string(), + raw_number: "300".into(), value: 300.0, start: 29, end: 32, @@ -837,7 +817,7 @@ fn moveto_smooth_curveto() { end: 33, }, SVGPathCSTNode::Number { - raw_number: "300".to_string(), + raw_number: "300".into(), value: 300.0, start: 33, end: 36, @@ -849,13 +829,13 @@ fn moveto_smooth_curveto() { chain_start: 4, chain_end: 36, }), - ] + ], ); } #[test] fn moveto_arc() { - assert_svg_path_cst!( + assert_svg_path_cst( b"m0 0a100,100 0 0 1 250,200 150 150 0 0 0 300 300", vec![ SVGPathCSTNode::Segment(SVGPathSegment { @@ -864,7 +844,7 @@ fn moveto_arc() { cst: vec![ SVGPathCSTNode::Command(&SVGPathCommand::MovetoLower), SVGPathCSTNode::Number { - raw_number: "0".to_string(), + raw_number: "0".into(), value: 0.0, start: 1, end: 2, @@ -875,7 +855,7 @@ fn moveto_arc() { end: 3, }, SVGPathCSTNode::Number { - raw_number: "0".to_string(), + raw_number: "0".into(), value: 0.0, start: 3, end: 4, @@ -893,14 +873,14 @@ fn moveto_arc() { cst: vec![ SVGPathCSTNode::Command(&SVGPathCommand::ArcLower), SVGPathCSTNode::Number { - raw_number: "100".to_string(), + raw_number: "100".into(), value: 100.0, start: 5, end: 8, }, SVGPathCSTNode::Comma { start: 8 }, SVGPathCSTNode::Number { - raw_number: "100".to_string(), + raw_number: "100".into(), value: 100.0, start: 9, end: 12, @@ -911,7 +891,7 @@ fn moveto_arc() { end: 13, }, SVGPathCSTNode::Number { - raw_number: "0".to_string(), + raw_number: "0".into(), value: 0.0, start: 13, end: 14, @@ -922,7 +902,7 @@ fn moveto_arc() { end: 15, }, SVGPathCSTNode::Number { - raw_number: "0".to_string(), + raw_number: "0".into(), value: 0.0, start: 15, end: 16, @@ -933,7 +913,7 @@ fn moveto_arc() { end: 17, }, SVGPathCSTNode::Number { - raw_number: "1".to_string(), + raw_number: "1".into(), value: 1.0, start: 17, end: 18, @@ -944,14 +924,14 @@ fn moveto_arc() { end: 19, }, SVGPathCSTNode::Number { - raw_number: "250".to_string(), + raw_number: "250".into(), value: 250.0, start: 19, end: 22, }, SVGPathCSTNode::Comma { start: 22 }, SVGPathCSTNode::Number { - raw_number: "200".to_string(), + raw_number: "200".into(), value: 200.0, start: 23, end: 26, @@ -973,7 +953,7 @@ fn moveto_arc() { args: vec![150.0, 150.0, 0.0, 0.0, 0.0, 300.0, 300.0], cst: vec![ SVGPathCSTNode::Number { - raw_number: "150".to_string(), + raw_number: "150".into(), value: 150.0, start: 27, end: 30, @@ -984,7 +964,7 @@ fn moveto_arc() { end: 31, }, SVGPathCSTNode::Number { - raw_number: "150".to_string(), + raw_number: "150".into(), value: 150.0, start: 31, end: 34, @@ -995,7 +975,7 @@ fn moveto_arc() { end: 35, }, SVGPathCSTNode::Number { - raw_number: "0".to_string(), + raw_number: "0".into(), value: 0.0, start: 35, end: 36, @@ -1006,7 +986,7 @@ fn moveto_arc() { end: 37, }, SVGPathCSTNode::Number { - raw_number: "0".to_string(), + raw_number: "0".into(), value: 0.0, start: 37, end: 38, @@ -1017,7 +997,7 @@ fn moveto_arc() { end: 39, }, SVGPathCSTNode::Number { - raw_number: "0".to_string(), + raw_number: "0".into(), value: 0.0, start: 39, end: 40, @@ -1028,7 +1008,7 @@ fn moveto_arc() { end: 41, }, SVGPathCSTNode::Number { - raw_number: "300".to_string(), + raw_number: "300".into(), value: 300.0, start: 41, end: 44, @@ -1039,7 +1019,7 @@ fn moveto_arc() { end: 45, }, SVGPathCSTNode::Number { - raw_number: "300".to_string(), + raw_number: "300".into(), value: 300.0, start: 45, end: 48, @@ -1051,25 +1031,25 @@ fn moveto_arc() { chain_start: 4, chain_end: 48, }), - ] + ], ); } #[test] fn invalid_arc_flag() { - assert_svg_path_cst_err!( + assert_svg_path_cst_err( b"m0 0a100,100 0 2 1 250,200", SyntaxError::InvalidArcFlag { index: 15, character: '2', command: 'a', - } + }, ); } #[test] fn moveto_quadratic() { - assert_svg_path_cst!( + assert_svg_path_cst( b"m0 0q100,100 250,200 150 150 300 300", vec![ SVGPathCSTNode::Segment(SVGPathSegment { @@ -1078,7 +1058,7 @@ fn moveto_quadratic() { cst: vec![ SVGPathCSTNode::Command(&SVGPathCommand::MovetoLower), SVGPathCSTNode::Number { - raw_number: "0".to_string(), + raw_number: "0".into(), value: 0.0, start: 1, end: 2, @@ -1089,7 +1069,7 @@ fn moveto_quadratic() { end: 3, }, SVGPathCSTNode::Number { - raw_number: "0".to_string(), + raw_number: "0".into(), value: 0.0, start: 3, end: 4, @@ -1107,14 +1087,14 @@ fn moveto_quadratic() { cst: vec![ SVGPathCSTNode::Command(&SVGPathCommand::QuadraticLower), SVGPathCSTNode::Number { - raw_number: "100".to_string(), + raw_number: "100".into(), value: 100.0, start: 5, end: 8, }, SVGPathCSTNode::Comma { start: 8 }, SVGPathCSTNode::Number { - raw_number: "100".to_string(), + raw_number: "100".into(), value: 100.0, start: 9, end: 12, @@ -1125,14 +1105,14 @@ fn moveto_quadratic() { end: 13, }, SVGPathCSTNode::Number { - raw_number: "250".to_string(), + raw_number: "250".into(), value: 250.0, start: 13, end: 16, }, SVGPathCSTNode::Comma { start: 16 }, SVGPathCSTNode::Number { - raw_number: "200".to_string(), + raw_number: "200".into(), value: 200.0, start: 17, end: 20, @@ -1154,7 +1134,7 @@ fn moveto_quadratic() { args: vec![150.0, 150.0, 300.0, 300.0], cst: vec![ SVGPathCSTNode::Number { - raw_number: "150".to_string(), + raw_number: "150".into(), value: 150.0, start: 21, end: 24, @@ -1165,7 +1145,7 @@ fn moveto_quadratic() { end: 25, }, SVGPathCSTNode::Number { - raw_number: "150".to_string(), + raw_number: "150".into(), value: 150.0, start: 25, end: 28, @@ -1176,7 +1156,7 @@ fn moveto_quadratic() { end: 29, }, SVGPathCSTNode::Number { - raw_number: "300".to_string(), + raw_number: "300".into(), value: 300.0, start: 29, end: 32, @@ -1187,7 +1167,7 @@ fn moveto_quadratic() { end: 33, }, SVGPathCSTNode::Number { - raw_number: "300".to_string(), + raw_number: "300".into(), value: 300.0, start: 33, end: 36, @@ -1199,13 +1179,13 @@ fn moveto_quadratic() { chain_start: 4, chain_end: 36, }), - ] + ], ); } #[test] fn moveto_smooth_quadratic() { - assert_svg_path_cst!( + assert_svg_path_cst( b"m0 0t100,100 250,200", vec![ SVGPathCSTNode::Segment(SVGPathSegment { @@ -1214,7 +1194,7 @@ fn moveto_smooth_quadratic() { cst: vec![ SVGPathCSTNode::Command(&SVGPathCommand::MovetoLower), SVGPathCSTNode::Number { - raw_number: "0".to_string(), + raw_number: "0".into(), value: 0.0, start: 1, end: 2, @@ -1225,7 +1205,7 @@ fn moveto_smooth_quadratic() { end: 3, }, SVGPathCSTNode::Number { - raw_number: "0".to_string(), + raw_number: "0".into(), value: 0.0, start: 3, end: 4, @@ -1243,14 +1223,14 @@ fn moveto_smooth_quadratic() { cst: vec![ SVGPathCSTNode::Command(&SVGPathCommand::SmoothQuadraticLower), SVGPathCSTNode::Number { - raw_number: "100".to_string(), + raw_number: "100".into(), value: 100.0, start: 5, end: 8, }, SVGPathCSTNode::Comma { start: 8 }, SVGPathCSTNode::Number { - raw_number: "100".to_string(), + raw_number: "100".into(), value: 100.0, start: 9, end: 12, @@ -1272,14 +1252,14 @@ fn moveto_smooth_quadratic() { args: vec![250.0, 200.0], cst: vec![ SVGPathCSTNode::Number { - raw_number: "250".to_string(), + raw_number: "250".into(), value: 250.0, start: 13, end: 16, }, SVGPathCSTNode::Comma { start: 16 }, SVGPathCSTNode::Number { - raw_number: "200".to_string(), + raw_number: "200".into(), value: 200.0, start: 17, end: 20, @@ -1291,25 +1271,25 @@ fn moveto_smooth_quadratic() { chain_start: 4, chain_end: 20, }), - ] + ], ); } #[test] fn invalid_multiple_commas() { - assert_svg_path_cst_err!( + assert_svg_path_cst_err( b"m0 0,,100,100", SyntaxError::InvalidNumber { - number: ",".to_string(), + number: ",".into(), start: 4, end: 5, - } + }, ); } #[test] fn arc_with_flags_together() { - assert_svg_path_cst!( + assert_svg_path_cst( b"m0 0a1.862 1.862 0 00-.248.033", vec![ SVGPathCSTNode::Segment(SVGPathSegment { @@ -1318,7 +1298,7 @@ fn arc_with_flags_together() { cst: vec![ SVGPathCSTNode::Command(&SVGPathCommand::MovetoLower), SVGPathCSTNode::Number { - raw_number: "0".to_string(), + raw_number: "0".into(), value: 0.0, start: 1, end: 2, @@ -1329,7 +1309,7 @@ fn arc_with_flags_together() { end: 3, }, SVGPathCSTNode::Number { - raw_number: "0".to_string(), + raw_number: "0".into(), value: 0.0, start: 3, end: 4, @@ -1347,7 +1327,7 @@ fn arc_with_flags_together() { cst: vec![ SVGPathCSTNode::Command(&SVGPathCommand::ArcLower), SVGPathCSTNode::Number { - raw_number: "1.862".to_string(), + raw_number: "1.862".into(), value: 1.862, start: 5, end: 10, @@ -1358,7 +1338,7 @@ fn arc_with_flags_together() { end: 11, }, SVGPathCSTNode::Number { - raw_number: "1.862".to_string(), + raw_number: "1.862".into(), value: 1.862, start: 11, end: 16, @@ -1369,7 +1349,7 @@ fn arc_with_flags_together() { end: 17, }, SVGPathCSTNode::Number { - raw_number: "0".to_string(), + raw_number: "0".into(), value: 0.0, start: 17, end: 18, @@ -1380,13 +1360,13 @@ fn arc_with_flags_together() { end: 19, }, SVGPathCSTNode::Number { - raw_number: "0".to_string(), + raw_number: "0".into(), value: 0.0, start: 19, end: 20, }, SVGPathCSTNode::Number { - raw_number: "0".to_string(), + raw_number: "0".into(), value: 0.0, start: 20, end: 21, @@ -1396,13 +1376,13 @@ fn arc_with_flags_together() { start: 21, }, SVGPathCSTNode::Number { - raw_number: ".248".to_string(), + raw_number: ".248".into(), value: 0.248, start: 22, end: 26, }, SVGPathCSTNode::Number { - raw_number: ".033".to_string(), + raw_number: ".033".into(), value: 0.033, start: 26, end: 30, @@ -1414,25 +1394,38 @@ fn arc_with_flags_together() { chain_start: 4, chain_end: 30, }), - ] + ], ); } #[test] fn invalid_utf8_length_1() { - assert_svg_path_cst_err!( + assert_svg_path_cst_err( b"m0 0\xE1", // \xE1 is á SyntaxError::InvalidCharacter { character: 'á', index: 4, - expected: "number or command".to_string() + expected: "number or command", + }, + ); +} + +#[test] +fn invalid_path_ending() { + // https://github.com/simple-icons/simple-icons/pull/11053 + assert_svg_path_cst_err( + b"m2.249 8.801a2.6 2.6 0 0 1-2.6 2.6 2.6 2.6 0 0 1-2.6-2.6 2.6 2.6 0 0 1 2.6-2.6 2.6 2.6 0 0 1 2.6 2.6z55", + SyntaxError::InvalidCharacter { + character: '5', + index: 101, + expected: "command", } ); } #[test] fn simple_icons_icon_path() { - let cst = svg_path_cst(include_bytes!("../fuzz/corpus/simpleicons.txt")); + let cst = svg_path_cst(include_bytes!("../../fuzz/corpus/simpleicons.txt")); assert!(cst.is_ok()); assert_eq!(cst.unwrap().len(), 46); } diff --git a/src/tests/strict.rs b/src/tests/strict.rs new file mode 100644 index 0000000..8b02cf5 --- /dev/null +++ b/src/tests/strict.rs @@ -0,0 +1,35 @@ +use crate::tests::helpers::*; +use crate::SyntaxError; + +#[test] +fn empty() { + assert_svg_path_cst_err( + b"", + SyntaxError::UnexpectedEnding { + expected: "moveto command", + index: 0, + }, + ); +} + +#[test] +fn none() { + assert_svg_path_cst_err( + b"none", + SyntaxError::ExpectedMovetoCommand { + character: 'n', + index: 0, + }, + ); +} + +#[test] +fn whitespaces() { + assert_svg_path_cst_err( + b"\t\n\r \x0C", + SyntaxError::ExpectedMovetoCommand { + character: '\t', + index: 0, + }, + ); +}