From c024e1bab89610455537b77aed249d2a05a81ed6 Mon Sep 17 00:00:00 2001 From: V0ldek Date: Thu, 28 Mar 2024 15:08:14 +0100 Subject: [PATCH] test: run JSONPath Compliance Test Suite on basic queries - CTS is now run in CI on queries that the engine supports. --- .github/workflows/book.yml | 15 +- .github/workflows/clusterfuzzlite-batch.yml | 5 +- .github/workflows/rust.yml | 11 +- Cargo.lock | 110 ++++++------ crates/rsonpath-lib/src/automaton.rs | 39 ++++- crates/rsonpath-lib/src/engine.rs | 2 +- crates/rsonpath-lib/src/engine/main.rs | 22 ++- .../{empty_query.rs => select_root_query.rs} | 0 crates/rsonpath-syntax/Cargo.toml | 2 +- .../jsonpath-compliance-test-suite | 2 +- crates/rsonpath-test/src/lib.rs | 165 +++++++++++++++++- crates/rsonpath-test/tests/cts.rs | 149 +++++++++++++--- 12 files changed, 408 insertions(+), 114 deletions(-) rename crates/rsonpath-lib/src/engine/{empty_query.rs => select_root_query.rs} (100%) diff --git a/.github/workflows/book.yml b/.github/workflows/book.yml index b2db29b8..14bd89c1 100644 --- a/.github/workflows/book.yml +++ b/.github/workflows/book.yml @@ -64,9 +64,20 @@ jobs: ~/.cargo/git/db/ target/ key: book-cargo-${{ hashFiles('**/Cargo.toml') }} - - name: Install mdbook + - name: cargo install mdbook if: steps.cache-restore-cargo.outputs.cache-hit != 'true' - run: cargo install mdbook mdbook-katex + uses: baptiste0928/cargo-install@94e1849646e5797d0c8b34d8e525124ae9ae1d86 # v3.0.1 + with: + # Name of the crate to install + crate: mdbook + env: + CARGO_TARGET_DIR: target/ + - name: cargo install mdbook-katex + if: steps.cache-restore-cargo.outputs.cache-hit != 'true' + uses: baptiste0928/cargo-install@94e1849646e5797d0c8b34d8e525124ae9ae1d86 # v3.0.1 + with: + # Name of the crate to install + crate: mdbook-katex env: CARGO_TARGET_DIR: target/ - name: Build the book diff --git a/.github/workflows/clusterfuzzlite-batch.yml b/.github/workflows/clusterfuzzlite-batch.yml index 549b53fa..da34c31d 100644 --- a/.github/workflows/clusterfuzzlite-batch.yml +++ b/.github/workflows/clusterfuzzlite-batch.yml @@ -67,7 +67,10 @@ jobs: uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 with: egress-policy: audit - + - name: Checkout sources + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + with: + submodules: true - name: Report crash id: report uses: JasonEtco/create-an-issue@1b14a70e4d8dc185e5cc76d3bec9eab20257b2c5 # v2.9.2 diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index a34836ec..277434ea 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -137,9 +137,12 @@ jobs: ~/.cargo/git/db/ target/ key: ${{ matrix.toolchain }}-${{ matrix.target_triple }}-cargo-${{ hashFiles('**/Cargo.toml') }} - - name: Install cargo-hack + - name: cargo install cargo-hack if: steps.cache-restore-cargo.outputs.cache-hit != 'true' - run: cargo install cargo-hack + uses: baptiste0928/cargo-install@94e1849646e5797d0c8b34d8e525124ae9ae1d86 # v3.0.1 + with: + # Name of the crate to install + crate: cargo-hack env: CARGO_TARGET_DIR: target/ - name: Build all feature sets @@ -359,9 +362,7 @@ jobs: with: submodules: true - name: cargo install cargo-msrv - # You may pin to the exact commit or the version. - # uses: baptiste0928/cargo-install@1cd874a5478fdca35d868ccc74640c5aabbb8f1b - uses: baptiste0928/cargo-install@v3.0.1 + uses: baptiste0928/cargo-install@94e1849646e5797d0c8b34d8e525124ae9ae1d86 # v3.0.1 with: # Name of the crate to install crate: cargo-msrv diff --git a/Cargo.lock b/Cargo.lock index ebf24a6d..ada7f3df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,9 +19,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "aho-corasick" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] @@ -91,15 +91,15 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" [[package]] name = "backtrace" -version = "0.3.69" +version = "0.3.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" dependencies = [ "addr2line", "cc", @@ -133,9 +133,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.2" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" [[package]] name = "camino" @@ -148,9 +148,9 @@ dependencies = [ [[package]] name = "cargo-platform" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "694c8807f2ae16faecc43dc17d74b3eb042482789fd0eb64b39a2e04e087053f" +checksum = "24b1f0365a6c6bb4020cd05806fd0d33c44d38046b8bd7f0e40814b9763cabfc" dependencies = [ "serde", ] @@ -183,9 +183,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "4.5.3" +version = "4.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "949626d00e063efc93b6dca932419ceb5432f99769911c0b995f7e884c778813" +checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" dependencies = [ "clap_builder", "clap_derive", @@ -206,14 +206,14 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.3" +version = "4.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90239a040c80f5e14809ca132ddc4176ab33d5e17e49691793296e3fcb34d72f" +checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.55", ] [[package]] @@ -325,7 +325,7 @@ checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.55", ] [[package]] @@ -380,9 +380,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.0.1" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" [[package]] name = "filetime" @@ -481,9 +481,9 @@ checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" [[package]] name = "indexmap" -version = "2.2.5" +version = "2.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", "hashbrown", @@ -491,15 +491,14 @@ dependencies = [ [[package]] name = "insta" -version = "1.36.1" +version = "1.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a7c22c4d34ef4788c351e971c52bfdfe7ea2766f8c5466bc175dd46e52ac22e" +checksum = "3eab73f58e59ca6526037208f0e98851159ec1633cf17b6cd2e1f2c3fd5d53cc" dependencies = [ "console", "lazy_static", "linked-hash-map", "similar", - "yaml-rust", ] [[package]] @@ -513,9 +512,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "lazy_static" @@ -706,7 +705,7 @@ checksum = "31b476131c3c86cb68032fdc5cb6d5a1045e3e42d96b69fa599fd77701e1f5bf" dependencies = [ "bit-set", "bit-vec", - "bitflags 2.4.2", + "bitflags 2.5.0", "lazy_static", "num-traits", "rand 0.8.5", @@ -815,9 +814,9 @@ dependencies = [ [[package]] name = "rayon" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4963ed1bc86e4f3ee217022bd855b297cef07fb9eac5dfa1f788b220b49b3bd" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" dependencies = [ "either", "rayon-core", @@ -844,9 +843,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.3" +version = "1.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" dependencies = [ "aho-corasick", "memchr", @@ -867,9 +866,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" [[package]] name = "rsonpath" @@ -966,17 +965,17 @@ dependencies = [ [[package]] name = "rustflags" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e74f1ec0cda1aea3a3a6c0df066cd67acac62e24064532b871e8eafb0ec6c126" +checksum = "e6d6e5ecce759f14333264b875a761e3d8159bcc79f527302b3537f17a9330ac" [[package]] name = "rustix" -version = "0.38.31" +version = "0.38.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" +checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", "errno", "libc", "linux-raw-sys", @@ -1042,14 +1041,14 @@ checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.55", ] [[package]] name = "serde_json" -version = "1.0.114" +version = "1.0.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" +checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd" dependencies = [ "itoa", "ryu", @@ -1091,9 +1090,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.13.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "snapbox" @@ -1151,9 +1150,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.52" +version = "2.0.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07" +checksum = "002a1b3dbf967edfafc32655d0f377ab0bb7b994aa1d32c8cc7e9b8bf3ebb8f0" dependencies = [ "proc-macro2", "quote", @@ -1200,7 +1199,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.55", ] [[package]] @@ -1211,7 +1210,7 @@ checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.55", "test-case-core", ] @@ -1232,7 +1231,7 @@ checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.55", ] [[package]] @@ -1270,9 +1269,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.11" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af06656561d28735e9c1cd63dfd57132c8155426aa6af24f36a00a351f88c48e" +checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3" dependencies = [ "serde", "serde_spanned", @@ -1291,9 +1290,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.7" +version = "0.22.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18769cd1cec395d70860ceb4d932812a0b4d06b1a4bb336745a4d21b9496e992" +checksum = "8e40bb779c5187258fd7aad0eb68cb8706a0a81fa712fbea808ab43c4b8374c4" dependencies = [ "indexmap", "serde", @@ -1576,15 +1575,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "yaml-rust" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" -dependencies = [ - "linked-hash-map", -] - [[package]] name = "yansi" version = "0.5.1" diff --git a/crates/rsonpath-lib/src/automaton.rs b/crates/rsonpath-lib/src/automaton.rs index bdbaddff..38ba9b5b 100644 --- a/crates/rsonpath-lib/src/automaton.rs +++ b/crates/rsonpath-lib/src/automaton.rs @@ -155,7 +155,7 @@ impl<'q> Automaton<'q> { Automaton::minimize(nfa) } - /// Returns whether this automaton represents an empty JSONPath query ('$'). + /// Returns whether this automaton represents the select-root JSONPath query ('$'). /// /// # Examples /// ```rust @@ -163,7 +163,7 @@ impl<'q> Automaton<'q> { /// let query = rsonpath_syntax::parse("$").unwrap(); /// let automaton = Automaton::new(&query).unwrap(); /// - /// assert!(automaton.is_empty_query()); + /// assert!(automaton.is_select_root_query()); /// ``` /// /// ```rust @@ -171,12 +171,47 @@ impl<'q> Automaton<'q> { /// let query = rsonpath_syntax::parse("$.a").unwrap(); /// let automaton = Automaton::new(&query).unwrap(); /// + /// assert!(!automaton.is_select_root_query()); + /// ``` + #[must_use] + #[inline(always)] + pub fn is_select_root_query(&self) -> bool { + self.states.len() == 2 + && self.states[1].array_transitions.is_empty() + && self.states[1].member_transitions.is_empty() + && self.states[1].fallback_state == State(0) + && self.states[1].attributes.is_accepting() + } + + /// Returns whether this automaton represents an empty JSONPath query that cannot accept anything. + /// + /// A query like this can be created by, for example, putting a trivially false filter + /// or an empty slice into the query. + /// + /// # Examples + /// ```rust + /// # use rsonpath::automaton::*; + /// let query = rsonpath_syntax::parse("$[::0]").unwrap(); + /// let automaton = Automaton::new(&query).unwrap(); + /// + /// assert!(automaton.is_empty_query()); + /// ``` + /// + /// ```rust + /// # use rsonpath::automaton::*; + /// let query = rsonpath_syntax::parse("$").unwrap(); + /// let automaton = Automaton::new(&query).unwrap(); + /// /// assert!(!automaton.is_empty_query()); /// ``` #[must_use] #[inline(always)] pub fn is_empty_query(&self) -> bool { self.states.len() == 2 + && self.states[1].array_transitions.is_empty() + && self.states[1].member_transitions.is_empty() + && self.states[1].fallback_state == State(0) + && !self.states[1].attributes.is_accepting() } /// Returns the rejecting state of the automaton. diff --git a/crates/rsonpath-lib/src/engine.rs b/crates/rsonpath-lib/src/engine.rs index 5f147ae6..d8456b9a 100644 --- a/crates/rsonpath-lib/src/engine.rs +++ b/crates/rsonpath-lib/src/engine.rs @@ -8,7 +8,7 @@ mod head_skipping; pub mod main; mod tail_skipping; pub use main::MainEngine as RsonpathEngine; -mod empty_query; +mod select_root_query; use self::error::EngineError; use crate::{ diff --git a/crates/rsonpath-lib/src/engine/main.rs b/crates/rsonpath-lib/src/engine/main.rs index f5bc9995..da929bb5 100644 --- a/crates/rsonpath-lib/src/engine/main.rs +++ b/crates/rsonpath-lib/src/engine/main.rs @@ -14,9 +14,9 @@ use crate::{ debug, depth::Depth, engine::{ - empty_query, error::EngineError, head_skipping::{CanHeadSkip, HeadSkip, ResumeState}, + select_root_query, tail_skipping::TailSkip, Compiler, Engine, Input, }, @@ -66,8 +66,11 @@ impl Engine for MainEngine<'_> { where I: Input, { + if self.automaton.is_select_root_query() { + return select_root_query::count(input); + } if self.automaton.is_empty_query() { - return empty_query::count(input); + return Ok(0); } let recorder = CountRecorder::new(); @@ -85,8 +88,11 @@ impl Engine for MainEngine<'_> { I: Input, S: Sink, { + if self.automaton.is_select_root_query() { + return select_root_query::index(input, sink); + } if self.automaton.is_empty_query() { - return empty_query::index(input, sink); + return Ok(()); } let recorder = IndexRecorder::new(sink, input.leading_padding_len()); @@ -104,8 +110,11 @@ impl Engine for MainEngine<'_> { I: Input, S: Sink, { + if self.automaton.is_select_root_query() { + return select_root_query::approx_span(input, sink); + } if self.automaton.is_empty_query() { - return empty_query::approx_span(input, sink); + return Ok(()); } let recorder = ApproxSpanRecorder::new(sink, input.leading_padding_len()); @@ -123,8 +132,11 @@ impl Engine for MainEngine<'_> { I: Input, S: Sink, { + if self.automaton.is_select_root_query() { + return select_root_query::match_(input, sink); + } if self.automaton.is_empty_query() { - return empty_query::match_(input, sink); + return Ok(()); } let recorder = NodesRecorder::build_recorder(sink, input.leading_padding_len()); diff --git a/crates/rsonpath-lib/src/engine/empty_query.rs b/crates/rsonpath-lib/src/engine/select_root_query.rs similarity index 100% rename from crates/rsonpath-lib/src/engine/empty_query.rs rename to crates/rsonpath-lib/src/engine/select_root_query.rs diff --git a/crates/rsonpath-syntax/Cargo.toml b/crates/rsonpath-syntax/Cargo.toml index 508475a8..04796465 100644 --- a/crates/rsonpath-syntax/Cargo.toml +++ b/crates/rsonpath-syntax/Cargo.toml @@ -24,7 +24,7 @@ thiserror = "1.0.58" unicode-width = "0.1.11" [dev-dependencies] -insta = "1.36.1" +insta = "1.38.0" pretty_assertions = "1.4.0" proptest = "1.4.0" test-case = "3.3.1" diff --git a/crates/rsonpath-test/jsonpath-compliance-test-suite b/crates/rsonpath-test/jsonpath-compliance-test-suite index 446336cd..7bd8532d 160000 --- a/crates/rsonpath-test/jsonpath-compliance-test-suite +++ b/crates/rsonpath-test/jsonpath-compliance-test-suite @@ -1 +1 @@ -Subproject commit 446336cd6651586f416a3b546c70bdd0fa2022c0 +Subproject commit 7bd8532dfcccd288bbcaa67b48e5804911991094 diff --git a/crates/rsonpath-test/src/lib.rs b/crates/rsonpath-test/src/lib.rs index 6407295d..ded81bc7 100644 --- a/crates/rsonpath-test/src/lib.rs +++ b/crates/rsonpath-test/src/lib.rs @@ -7,31 +7,86 @@ pub enum Tag { Basic, Filter, Function, + MultipleSelectors, + IndexingFromEnd, + BackwardStep, + ProperUnicode, + StrictDescendantOrder, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TestSuite { - tests: Vec, + tests: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TestCase { +pub struct TestCaseDef { pub name: String, pub selector: String, #[serde(default)] pub document: serde_json::Value, #[serde(default)] - pub result: Vec, + pub result: Option>, + #[serde(default)] + pub results: Option>>, #[serde(default)] pub invalid_selector: bool, } +#[derive(Debug, Clone)] +pub struct TestCase { + pub name: String, + pub details: TestCaseDetails, +} + +#[derive(Debug, Clone)] +pub enum TestCaseDetails { + Invalid(InvalidTestCase), + Valid(ValidTestCase), +} + +#[derive(Debug, Clone)] +pub struct InvalidTestCase { + pub selector: String, +} + +#[derive(Debug, Clone)] +pub struct ValidTestCase { + pub selector: String, + pub document: serde_json::Value, + pub results: Vec>, +} + #[derive(Debug, Clone)] pub struct TaggedTestCase { - pub tag: Tag, + pub tags: Vec, pub test_case: TestCase, } +impl From for TestCase { + fn from(value: TestCaseDef) -> Self { + let name = value.name; + let details = if value.invalid_selector { + TestCaseDetails::Invalid(InvalidTestCase { + selector: value.selector, + }) + } else { + let results = match (value.result, value.results) { + (Some(result), None) => vec![result], + (None, Some(results)) => results, + (Some(_), Some(_)) => panic!("{name}: both 'result' and 'results' defined"), + (None, None) => panic!("{name}: neither 'result' nor 'results' defined"), + }; + TestCaseDetails::Valid(ValidTestCase { + selector: value.selector, + document: value.document, + results, + }) + }; + Self { name, details } + } +} + /// Read and tag test cases from the base jsonpath-compliance-test-suite path. pub fn read_and_tag>(path: P) -> Result, io::Error> { let tests = path.as_ref().join("tests"); @@ -77,6 +132,98 @@ pub fn read_and_tag>(path: P) -> Result, io:: // This is included in /filter.json, but contains function calls. collection.add_special_case_tag("equals, special nothing", Tag::Function); + // Tests with multiple selectors. + let tests = [ + "multiple selectors", + "multiple selectors, name and index, array data", + "multiple selectors, name and index, object data", + "multiple selectors, index and slice", + "multiple selectors, index and slice, overlapping", + "multiple selectors, duplicate index", + "multiple selectors, wildcard and index", + "multiple selectors, wildcard and name", + "multiple selectors, wildcard and slice", + "multiple selectors, multiple wildcards", + "descendant segment, multiple selectors", + "descendant segment, object traversal, multiple selectors", + "space between selector and comma", + "newline between selector and comma", + "tab between selector and comma", + "return between selector and comma", + "space between comma and selector", + "newline between comma and selector", + "tab between comma and selector", + "return between comma and selector", + ]; + for test in tests { + collection.add_special_case_tag(test, Tag::MultipleSelectors); + } + + // Tests with indexing from end. + let tests = [ + "negative", + "more negative", + "negative out of bound", + "negative range with default step", + "negative range with negative step", + "negative range with larger negative step", + "larger negative range with larger negative step", + "negative from, positive to", + "negative from", + "positive from, negative to", + "negative from, positive to, negative step", + "positive from, negative to, negative step", + "excessively small from value", + "excessively large from value with negative step", + "excessively small to value with negative step", + "excessively small step", + ]; + for test in tests { + collection.add_special_case_tag(test, Tag::IndexingFromEnd); + } + + // Tests with backwards step. + let tests = [ + "negative step with default start and end", + "negative step with default start", + "negative step with default end", + "larger negative step", + "negative step with empty array", + "maximal range with negative step", + ]; + for test in tests { + collection.add_special_case_tag(test, Tag::BackwardStep); + } + + // Tests that require proper unicode support. + let tests = [ + "double quotes, escaped double quote", + "double quotes, escaped reverse solidus", + "double quotes, escaped backspace", + "double quotes, escaped form feed", + "double quotes, escaped line feed", + "double quotes, escaped carriage return", + "double quotes, escaped tab", + "single quotes, escaped reverse solidus", + "single quotes, escaped backspace", + "single quotes, escaped form feed", + "single quotes, escaped line feed", + "single quotes, escaped carriage return", + "single quotes, escaped tab", + ]; + for test in tests { + collection.add_special_case_tag(test, Tag::ProperUnicode); + } + + // Tests that require quite insane ordering semantics from descendant. + let tests = [ + "descendant segment, wildcard selector, nested arrays", + "descendant segment, wildcard selector, nested objects", + ]; + for test in tests { + collection.add_special_case_tag(test, Tag::StrictDescendantOrder); + } + Ok(collection.get()) } @@ -93,8 +240,12 @@ impl TaggedTestCollection { let file = File::open(file.as_ref())?; let deser: TestSuite = serde_json::from_reader(file)?; - for test_case in deser.tests { - self.cases.push(TaggedTestCase { tag, test_case }) + for test_case_def in deser.tests { + let test_case = test_case_def.into(); + self.cases.push(TaggedTestCase { + tags: vec![tag], + test_case, + }) } Ok(()) @@ -106,7 +257,7 @@ impl TaggedTestCollection { .iter_mut() .find(|x| x.test_case.name == name) .expect("invalid special-case name"); - case.tag = tag; + case.tags.push(tag); } fn get(self) -> Vec { diff --git a/crates/rsonpath-test/tests/cts.rs b/crates/rsonpath-test/tests/cts.rs index aab4f2a0..42c2755f 100644 --- a/crates/rsonpath-test/tests/cts.rs +++ b/crates/rsonpath-test/tests/cts.rs @@ -1,13 +1,17 @@ +use rsonpath::engine::{Compiler, Engine}; +use rsonpath_test::{Tag, TaggedTestCase, TestCaseDetails}; +use serde_json::Value; use std::io; -use rsonpath_test::{Tag, TaggedTestCase}; - const CTS_PATH: &str = "jsonpath-compliance-test-suite"; #[test] fn test_cts() -> Result<(), io::Error> { let collection = rsonpath_test::read_and_tag(CTS_PATH)?; - let results: Vec<_> = collection.into_iter().map(test_one).collect(); + let results: Vec<_> = collection + .into_iter() + .map(|t| (t.test_case.name.clone(), test_one(t))) + .collect(); let mut success = true; for (name, result) in results { @@ -26,42 +30,129 @@ fn test_cts() -> Result<(), io::Error> { Ok(()) } -fn test_one(t: TaggedTestCase) -> (String, TestResult) { - let (tag, test_case) = (t.tag, t.test_case); - if !does_parser_support(tag) { - return (test_case.name, TestResult::Ignored); +fn test_one(def: TaggedTestCase) -> TestResult { + let (tags, test_case) = (def.tags, def.test_case); + if !does_parser_support(&tags) { + return TestResult::Ignored; + } + + match test_case.details { + TestCaseDetails::Invalid(test_details) => { + let parser_result = rsonpath_syntax::parse(&test_details.selector); + if parser_result.is_ok() { + let err = format!( + "test case {} is supposed to fail, but parser accepted the query\nparse result: {:?}", + test_case.name, + parser_result.unwrap() + ); + TestResult::Failed(err) + } else { + TestResult::Passed + } + } + TestCaseDetails::Valid(test_details) => { + let parser_result = rsonpath_syntax::parse(&test_details.selector); + match parser_result { + Ok(query) => { + if !does_engine_support(&tags) { + TestResult::Ignored + } else { + match rsonpath::engine::RsonpathEngine::compile_query(&query) { + Ok(engine) => { + let input_str = test_details.document.to_string(); + let input = rsonpath::input::OwnedBytes::from(input_str); + let mut results = vec![]; + match engine.matches(&input, &mut results) { + Ok(()) => match compare_results(&results, &test_details.results) { + Ok(()) => TestResult::Passed, + Err(err) => { + let err = + format!("test case {} failed\ninvalid result: {}", test_case.name, err); + TestResult::Failed(err) + } + }, + Err(engine_err) => { + let err = format!( + "test case {} failed\nexecution error: {}", + test_case.name, engine_err + ); + TestResult::Failed(err) + } + } + } + Err(compile_err) => { + let err = format!( + "test case {} failed to compile\ncompile error: {}", + test_case.name, compile_err + ); + TestResult::Failed(err) + } + } + } + } + Err(parse_err) => { + let err = format!( + "test case {} failed to parse\nparse error: {}", + test_case.name, parse_err + ); + TestResult::Failed(err) + } + } + } } +} - let parser_result = rsonpath_syntax::parse(&test_case.selector); +fn compare_results(matches: &[rsonpath::result::Match], variants: &Vec>) -> Result<(), String> { + assert!(!variants.is_empty()); + let actual: Result, _> = matches.iter().map(|m| serde_json::from_slice(m.bytes())).collect(); + let actual = actual.map_err(|err| format!("matched value is not a valid JSON: {err}"))?; - if test_case.invalid_selector { - if parser_result.is_ok() { - let err = format!( - "test case {} is supposed to fail, but parser accepted the query\nparse result: {:?}", - test_case.name, - parser_result.unwrap() - ); - return (test_case.name, TestResult::Failed(err)); + for variant in variants { + if variant == &actual { + return Ok(()); } - return (test_case.name, TestResult::Passed); } - if parser_result.is_err() { - let err = format!( - "test case {} failed to parse\nparse error: {}", - test_case.name, - parser_result.unwrap_err() - ); - return (test_case.name, TestResult::Failed(err)); + let diff = pretty_assertions::Comparison::new(&variants[0], &actual); + + if variants.len() == 1 { + Err(diff.to_string()) + } else { + Err(format!("no result variants matched; diff with first:\n{diff}",)) } +} - (test_case.name, TestResult::Passed) +fn does_parser_support(tags: &[Tag]) -> bool { + return tags.iter().all(single); + + fn single(tag: &Tag) -> bool { + match tag { + Tag::Basic + | Tag::Filter + | Tag::MultipleSelectors + | Tag::IndexingFromEnd + | Tag::BackwardStep + | Tag::ProperUnicode => true, + Tag::StrictDescendantOrder => true, + Tag::Function => false, + } + } } -fn does_parser_support(tag: Tag) -> bool { - match tag { - Tag::Basic | Tag::Filter => true, - Tag::Function => false, +fn does_engine_support(tags: &[Tag]) -> bool { + return tags.iter().all(single); + + fn single(tag: &Tag) -> bool { + match tag { + Tag::Basic => true, + Tag::Filter + | Tag::Function + | Tag::MultipleSelectors + | Tag::IndexingFromEnd + | Tag::BackwardStep + | Tag::StrictDescendantOrder + | Tag::ProperUnicode => false, + } } }