From b699f4120ee55cfcae7c89d1ac6ed2a9a3f7936a Mon Sep 17 00:00:00 2001 From: Jarvis Date: Mon, 18 May 2026 17:30:41 +0800 Subject: [PATCH 1/7] test: add cjson and simdjson fixture coverage --- tests/fixtures/third_party/README.md | 26 ++ tests/fixtures/third_party/cjson/test1.json | 22 ++ tests/fixtures/third_party/cjson/test10.json | 1 + tests/fixtures/third_party/cjson/test11.json | 8 + tests/fixtures/third_party/cjson/test2.json | 11 + tests/fixtures/third_party/cjson/test9.json | 5 + .../third_party/simdjson/example_config.json | 11 + tests/lua/cjson_compat_spec.lua | 52 ++++ tests/third_party_fixtures.rs | 227 ++++++++++++++++++ 9 files changed, 363 insertions(+) create mode 100644 tests/fixtures/third_party/README.md create mode 100644 tests/fixtures/third_party/cjson/test1.json create mode 100644 tests/fixtures/third_party/cjson/test10.json create mode 100644 tests/fixtures/third_party/cjson/test11.json create mode 100644 tests/fixtures/third_party/cjson/test2.json create mode 100644 tests/fixtures/third_party/cjson/test9.json create mode 100644 tests/fixtures/third_party/simdjson/example_config.json create mode 100644 tests/third_party_fixtures.rs diff --git a/tests/fixtures/third_party/README.md b/tests/fixtures/third_party/README.md new file mode 100644 index 0000000..b8c2de5 --- /dev/null +++ b/tests/fixtures/third_party/README.md @@ -0,0 +1,26 @@ +# Third-party JSON fixtures + +This directory contains a small, selected corpus adapted from upstream JSON +parser test data. The C/C++ test harnesses are not vendored; qjson consumes +these files through its own Rust and Lua test suites. + +## DaveGamble/cJSON + +- Source: https://github.com/DaveGamble/cJSON +- Upstream paths: `tests/inputs/test1`, `test2`, `test9`, `test10`, `test11` +- License: MIT +- Copyright: Copyright (c) 2009-2017 Dave Gamble and cJSON contributors + +The MIT license notice from upstream requires preserving the copyright and +permission notice with copied substantial portions. + +## simdjson/simdjson + +- Source: https://github.com/simdjson/simdjson +- Upstream paths: `jsonexamples/example_config.json` plus selected DOM test + literals from `tests/dom/big_integer_tests.cpp` +- License choice for copied material: MIT +- Copyright: Copyright 2018-2025 The simdjson authors + +simdjson is dual-licensed under Apache-2.0 and MIT; qjson uses the MIT option +for this copied test material. diff --git a/tests/fixtures/third_party/cjson/test1.json b/tests/fixtures/third_party/cjson/test1.json new file mode 100644 index 0000000..eacfbf5 --- /dev/null +++ b/tests/fixtures/third_party/cjson/test1.json @@ -0,0 +1,22 @@ +{ + "glossary": { + "title": "example glossary", + "GlossDiv": { + "title": "S", + "GlossList": { + "GlossEntry": { + "ID": "SGML", + "SortAs": "SGML", + "GlossTerm": "Standard Generalized Markup Language", + "Acronym": "SGML", + "Abbrev": "ISO 8879:1986", + "GlossDef": { + "para": "A meta-markup language, used to create markup languages such as DocBook.", + "GlossSeeAlso": ["GML", "XML"] + }, + "GlossSee": "markup" + } + } + } + } +} diff --git a/tests/fixtures/third_party/cjson/test10.json b/tests/fixtures/third_party/cjson/test10.json new file mode 100644 index 0000000..d19eb8b --- /dev/null +++ b/tests/fixtures/third_party/cjson/test10.json @@ -0,0 +1 @@ +["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"] diff --git a/tests/fixtures/third_party/cjson/test11.json b/tests/fixtures/third_party/cjson/test11.json new file mode 100644 index 0000000..039c61b --- /dev/null +++ b/tests/fixtures/third_party/cjson/test11.json @@ -0,0 +1,8 @@ +{ +"name": "Jack (\"Bee\") Nimble", +"format": {"type": "rect", +"width": 1920, +"height": 1080, +"interlace": false,"frame rate": 24 +} +} diff --git a/tests/fixtures/third_party/cjson/test2.json b/tests/fixtures/third_party/cjson/test2.json new file mode 100644 index 0000000..5600991 --- /dev/null +++ b/tests/fixtures/third_party/cjson/test2.json @@ -0,0 +1,11 @@ +{"menu": { + "id": "file", + "value": "File", + "popup": { + "menuitem": [ + {"value": "New", "onclick": "CreateNewDoc()"}, + {"value": "Open", "onclick": "OpenDoc()"}, + {"value": "Close", "onclick": "CloseDoc()"} + ] + } +}} diff --git a/tests/fixtures/third_party/cjson/test9.json b/tests/fixtures/third_party/cjson/test9.json new file mode 100644 index 0000000..2a939b9 --- /dev/null +++ b/tests/fixtures/third_party/cjson/test9.json @@ -0,0 +1,5 @@ +[ + [0, -1, 0], + [1, 0, 0], + [0, 0, 1] + ] diff --git a/tests/fixtures/third_party/simdjson/example_config.json b/tests/fixtures/third_party/simdjson/example_config.json new file mode 100644 index 0000000..25c96f2 --- /dev/null +++ b/tests/fixtures/third_party/simdjson/example_config.json @@ -0,0 +1,11 @@ +{ + "app_name": "MyApp", + "version": "1.0.0", + "port": 8080, + "debug": true, + "features": ["logging", "caching"], + "database": { + "host": "localhost", + "port": 5432 + } +} diff --git a/tests/lua/cjson_compat_spec.lua b/tests/lua/cjson_compat_spec.lua index 9498b35..643fe1f 100644 --- a/tests/lua/cjson_compat_spec.lua +++ b/tests/lua/cjson_compat_spec.lua @@ -1,6 +1,36 @@ local qjson = require("qjson") local cjson = require("cjson") +local function read_file(path) + local f = assert(io.open(path, "rb")) + local s = f:read("*a") + f:close() + return s +end + +local function deep_equal(a, b) + if a == b then + return true + end + if type(a) ~= type(b) then + return false + end + if type(a) ~= "table" then + return false + end + for k, v in pairs(a) do + if not deep_equal(v, b[k]) then + return false + end + end + for k in pairs(b) do + if a[k] == nil then + return false + end + end + return true +end + describe("qjson vs lua-cjson", function() it("agrees on simple string field", function() local s = '{"a":"x"}' @@ -26,4 +56,26 @@ describe("qjson vs lua-cjson", function() local s = '{"body":{"model":"gpt"}}' assert.are.equal(cjson.decode(s).body.model, qjson.parse(s):get_str("body.model")) end) + + local fixture_paths = { + "tests/fixtures/third_party/cjson/test1.json", + "tests/fixtures/third_party/cjson/test2.json", + "tests/fixtures/third_party/cjson/test9.json", + "tests/fixtures/third_party/cjson/test10.json", + "tests/fixtures/third_party/cjson/test11.json", + "tests/fixtures/third_party/simdjson/example_config.json", + } + + for _, path in ipairs(fixture_paths) do + it("materializes like lua-cjson for fixture " .. path, function() + local src = read_file(path) + assert.is_true(deep_equal(qjson.materialize(qjson.decode(src)), cjson.decode(src))) + end) + + it("encodes a lua-cjson-equivalent value for fixture " .. path, function() + local src = read_file(path) + local out = qjson.encode(qjson.decode(src)) + assert.is_true(deep_equal(cjson.decode(out), cjson.decode(src))) + end) + end end) diff --git a/tests/third_party_fixtures.rs b/tests/third_party_fixtures.rs new file mode 100644 index 0000000..bc88b62 --- /dev/null +++ b/tests/third_party_fixtures.rs @@ -0,0 +1,227 @@ +use std::os::raw::c_int; +use std::ptr; + +use qjson::doc::Document; +use qjson::error::qjson_err; +use qjson::ffi::*; +use qjson::options::{Options, QJSON_MODE_EAGER, QJSON_MODE_LAZY}; + +const CJSON_FIXTURES: &[(&str, &[u8])] = &[ + ( + "cjson/test1.json", + include_bytes!("fixtures/third_party/cjson/test1.json"), + ), + ( + "cjson/test2.json", + include_bytes!("fixtures/third_party/cjson/test2.json"), + ), + ( + "cjson/test9.json", + include_bytes!("fixtures/third_party/cjson/test9.json"), + ), + ( + "cjson/test10.json", + include_bytes!("fixtures/third_party/cjson/test10.json"), + ), + ( + "cjson/test11.json", + include_bytes!("fixtures/third_party/cjson/test11.json"), + ), +]; + +const SIMDJSON_EXAMPLE_CONFIG: &[u8] = + include_bytes!("fixtures/third_party/simdjson/example_config.json"); + +fn parse(s: &[u8]) -> *mut qjson_doc { + let mut err: c_int = -1; + let d = unsafe { qjson_parse(s.as_ptr(), s.len(), &mut err) }; + assert_eq!(err, qjson_err::QJSON_OK as c_int); + assert!(!d.is_null()); + d +} + +fn get_str(doc: *mut qjson_doc, path: &[u8]) -> String { + let mut p: *const u8 = ptr::null(); + let mut n: usize = 0; + let rc = unsafe { qjson_get_str(doc, path.as_ptr() as *const i8, path.len(), &mut p, &mut n) }; + assert_eq!(rc, qjson_err::QJSON_OK as c_int); + String::from_utf8(unsafe { std::slice::from_raw_parts(p, n) }.to_vec()).unwrap() +} + +fn get_i64(doc: *mut qjson_doc, path: &[u8]) -> i64 { + let mut v: i64 = 0; + let rc = unsafe { qjson_get_i64(doc, path.as_ptr() as *const i8, path.len(), &mut v) }; + assert_eq!(rc, qjson_err::QJSON_OK as c_int); + v +} + +fn get_bool(doc: *mut qjson_doc, path: &[u8]) -> bool { + let mut v: c_int = -1; + let rc = unsafe { qjson_get_bool(doc, path.as_ptr() as *const i8, path.len(), &mut v) }; + assert_eq!(rc, qjson_err::QJSON_OK as c_int); + v != 0 +} + +fn len(doc: *mut qjson_doc, path: &[u8]) -> usize { + let mut n: usize = 0; + let rc = unsafe { qjson_len(doc, path.as_ptr() as *const i8, path.len(), &mut n) }; + assert_eq!(rc, qjson_err::QJSON_OK as c_int); + n +} + +fn open(doc: *mut qjson_doc, path: &[u8]) -> qjson_cursor { + let mut cur = std::mem::MaybeUninit::::uninit(); + let rc = unsafe { + qjson_open( + doc, + path.as_ptr() as *const i8, + path.len(), + cur.as_mut_ptr(), + ) + }; + assert_eq!(rc, qjson_err::QJSON_OK as c_int); + unsafe { cur.assume_init() } +} + +fn cursor_index(cur: &qjson_cursor, index: usize) -> qjson_cursor { + let mut sub = std::mem::MaybeUninit::::uninit(); + let rc = unsafe { qjson_cursor_index(cur, index, sub.as_mut_ptr()) }; + assert_eq!(rc, qjson_err::QJSON_OK as c_int); + unsafe { sub.assume_init() } +} + +fn cursor_get_str(cur: &qjson_cursor) -> String { + let empty = b""; + let mut p: *const u8 = ptr::null(); + let mut n: usize = 0; + let rc = unsafe { qjson_cursor_get_str(cur, empty.as_ptr() as *const i8, 0, &mut p, &mut n) }; + assert_eq!(rc, qjson_err::QJSON_OK as c_int); + String::from_utf8(unsafe { std::slice::from_raw_parts(p, n) }.to_vec()).unwrap() +} + +#[test] +fn cjson_fixtures_parse_in_both_modes() { + let eager = Options { + mode: QJSON_MODE_EAGER, + max_depth: 0, + }; + let lazy = Options { + mode: QJSON_MODE_LAZY, + max_depth: 0, + }; + + for (name, data) in CJSON_FIXTURES { + Document::parse_with_options(data, &eager) + .unwrap_or_else(|e| panic!("{name} rejected in eager mode: {e:?}")); + Document::parse_with_options(data, &lazy) + .unwrap_or_else(|e| panic!("{name} rejected in lazy mode: {e:?}")); + } +} + +#[test] +fn cjson_nested_object_fixture_paths_are_accessible() { + let doc = parse(include_bytes!("fixtures/third_party/cjson/test1.json")); + + assert_eq!(get_str(doc, b"glossary.title"), "example glossary"); + assert_eq!( + get_str(doc, b"glossary.GlossDiv.GlossList.GlossEntry.ID"), + "SGML" + ); + assert_eq!( + get_str(doc, b"glossary.GlossDiv.GlossList.GlossEntry.GlossDef.para"), + "A meta-markup language, used to create markup languages such as DocBook." + ); + + let see_also = b"glossary.GlossDiv.GlossList.GlossEntry.GlossDef.GlossSeeAlso"; + assert_eq!(len(doc, see_also), 2); + let cur = open(doc, see_also); + assert_eq!(cursor_get_str(&cursor_index(&cur, 0)), "GML"); + assert_eq!(cursor_get_str(&cursor_index(&cur, 1)), "XML"); + + unsafe { qjson_free(doc) }; +} + +#[test] +fn cjson_menu_and_matrix_fixtures_keep_array_shape() { + let menu_doc = parse(include_bytes!("fixtures/third_party/cjson/test2.json")); + assert_eq!(get_str(menu_doc, b"menu.id"), "file"); + + let items = open(menu_doc, b"menu.popup.menuitem"); + assert_eq!(len(menu_doc, b"menu.popup.menuitem"), 3); + let second = cursor_index(&items, 1); + let mut onclick = std::mem::MaybeUninit::::uninit(); + let rc = unsafe { + qjson_cursor_field( + &second, + b"onclick".as_ptr() as *const i8, + b"onclick".len(), + onclick.as_mut_ptr(), + ) + }; + assert_eq!(rc, qjson_err::QJSON_OK as c_int); + assert_eq!( + cursor_get_str(&unsafe { onclick.assume_init() }), + "OpenDoc()" + ); + unsafe { qjson_free(menu_doc) }; + + let matrix_doc = parse(include_bytes!("fixtures/third_party/cjson/test9.json")); + let root = open(matrix_doc, b""); + assert_eq!(len(matrix_doc, b""), 3); + let middle_row = cursor_index(&root, 1); + let first = cursor_index(&middle_row, 0); + let mut v: i64 = 0; + let empty = b""; + let rc = unsafe { qjson_cursor_get_i64(&first, empty.as_ptr() as *const i8, 0, &mut v) }; + assert_eq!(rc, qjson_err::QJSON_OK as c_int); + assert_eq!(v, 1); + unsafe { qjson_free(matrix_doc) }; +} + +#[test] +fn cjson_escaped_string_and_spaced_key_fixture_paths_are_accessible() { + let doc = parse(include_bytes!("fixtures/third_party/cjson/test11.json")); + + assert_eq!(get_str(doc, b"name"), "Jack (\"Bee\") Nimble"); + assert_eq!(get_str(doc, b"format.type"), "rect"); + assert_eq!(get_i64(doc, b"format.width"), 1920); + assert_eq!(get_i64(doc, b"format.height"), 1080); + assert!(!get_bool(doc, b"format.interlace")); + assert_eq!(get_i64(doc, b"format.frame rate"), 24); + + unsafe { qjson_free(doc) }; +} + +#[test] +fn simdjson_example_config_fixture_paths_are_accessible() { + let doc = parse(SIMDJSON_EXAMPLE_CONFIG); + + assert_eq!(get_str(doc, b"app_name"), "MyApp"); + assert_eq!(get_str(doc, b"version"), "1.0.0"); + assert_eq!(get_i64(doc, b"port"), 8080); + assert!(get_bool(doc, b"debug")); + assert_eq!(len(doc, b"features"), 2); + assert_eq!(get_str(doc, b"database.host"), "localhost"); + assert_eq!(get_i64(doc, b"database.port"), 5432); + + unsafe { qjson_free(doc) }; +} + +#[test] +fn simdjson_big_integer_literals_parse_but_do_not_fit_i64() { + let cases = [ + br#"{"val":123456789012345678901}"#.as_slice(), + br#"{"val":-12345678901234567890}"#.as_slice(), + br#"[1, 123456789012345678901, 3]"#.as_slice(), + ]; + + for data in cases { + Document::parse(data).unwrap(); + } + + let doc = parse(cases[0]); + let mut v: i64 = 0; + let rc = unsafe { qjson_get_i64(doc, b"val".as_ptr() as *const i8, b"val".len(), &mut v) }; + assert_eq!(rc, qjson_err::QJSON_OUT_OF_RANGE as c_int); + unsafe { qjson_free(doc) }; +} From e23457bccba3d0ccf7dc1c634fc18aa98bc2f45d Mon Sep 17 00:00:00 2001 From: Jarvis Date: Mon, 18 May 2026 17:43:01 +0800 Subject: [PATCH 2/7] test: reuse upstream fixture submodules --- .github/workflows/ci.yml | 2 + .gitmodules | 6 + tests/fixtures/third_party/README.md | 35 +-- tests/fixtures/third_party/cjson/test1.json | 22 -- tests/fixtures/third_party/cjson/test10.json | 1 - tests/fixtures/third_party/cjson/test11.json | 8 - tests/fixtures/third_party/cjson/test2.json | 11 - tests/fixtures/third_party/cjson/test9.json | 5 - .../third_party/simdjson/example_config.json | 11 - tests/lua/cjson_compat_spec.lua | 29 +- tests/third_party_fixtures.rs | 250 +++++++++++++++--- tests/vendor/cJSON | 1 + tests/vendor/simdjson | 1 + 13 files changed, 249 insertions(+), 133 deletions(-) delete mode 100644 tests/fixtures/third_party/cjson/test1.json delete mode 100644 tests/fixtures/third_party/cjson/test10.json delete mode 100644 tests/fixtures/third_party/cjson/test11.json delete mode 100644 tests/fixtures/third_party/cjson/test2.json delete mode 100644 tests/fixtures/third_party/cjson/test9.json delete mode 100644 tests/fixtures/third_party/simdjson/example_config.json create mode 160000 tests/vendor/cJSON create mode 160000 tests/vendor/simdjson diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2537181..4f2595a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,6 +54,8 @@ jobs: needs: rust steps: - uses: actions/checkout@v4 + with: + submodules: recursive - name: Install Rust (stable) run: | diff --git a/.gitmodules b/.gitmodules index 8baae4a..3f986cb 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,9 @@ [submodule "tests/vendor/JSONTestSuite"] path = tests/vendor/JSONTestSuite url = https://github.com/nst/JSONTestSuite +[submodule "tests/vendor/cJSON"] + path = tests/vendor/cJSON + url = https://github.com/DaveGamble/cJSON.git +[submodule "tests/vendor/simdjson"] + path = tests/vendor/simdjson + url = https://github.com/simdjson/simdjson.git diff --git a/tests/fixtures/third_party/README.md b/tests/fixtures/third_party/README.md index b8c2de5..a5719ab 100644 --- a/tests/fixtures/third_party/README.md +++ b/tests/fixtures/third_party/README.md @@ -1,26 +1,19 @@ -# Third-party JSON fixtures +# Third-party JSON fixture sources -This directory contains a small, selected corpus adapted from upstream JSON -parser test data. The C/C++ test harnesses are not vendored; qjson consumes -these files through its own Rust and Lua test suites. +qjson reuses mature upstream JSON test data through git submodules instead of +copying large C/C++ test harnesses into this repository. -## DaveGamble/cJSON +- `tests/vendor/cJSON`: `DaveGamble/cJSON`, MIT licensed. Rust and Lua tests + consume `tests/inputs/*` fixtures with matching `.expected` files, and Rust + ports selected parser literals from cJSON number/string/array tests. +- `tests/vendor/simdjson`: `simdjson/simdjson`, dual Apache-2.0/MIT licensed. + qjson uses the MIT option and consumes the single-document `.json` files in + `jsonexamples/`; the `.ndjson` streaming example is intentionally excluded. -- Source: https://github.com/DaveGamble/cJSON -- Upstream paths: `tests/inputs/test1`, `test2`, `test9`, `test10`, `test11` -- License: MIT -- Copyright: Copyright (c) 2009-2017 Dave Gamble and cJSON contributors +The upstream submodules carry their own license files: -The MIT license notice from upstream requires preserving the copyright and -permission notice with copied substantial portions. +- `tests/vendor/cJSON/LICENSE` +- `tests/vendor/simdjson/LICENSE-MIT` -## simdjson/simdjson - -- Source: https://github.com/simdjson/simdjson -- Upstream paths: `jsonexamples/example_config.json` plus selected DOM test - literals from `tests/dom/big_integer_tests.cpp` -- License choice for copied material: MIT -- Copyright: Copyright 2018-2025 The simdjson authors - -simdjson is dual-licensed under Apache-2.0 and MIT; qjson uses the MIT option -for this copied test material. +The local Rust and Lua harnesses are qjson tests; the upstream C/C++ harnesses +are left in the submodules as source material rather than compiled here. diff --git a/tests/fixtures/third_party/cjson/test1.json b/tests/fixtures/third_party/cjson/test1.json deleted file mode 100644 index eacfbf5..0000000 --- a/tests/fixtures/third_party/cjson/test1.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "glossary": { - "title": "example glossary", - "GlossDiv": { - "title": "S", - "GlossList": { - "GlossEntry": { - "ID": "SGML", - "SortAs": "SGML", - "GlossTerm": "Standard Generalized Markup Language", - "Acronym": "SGML", - "Abbrev": "ISO 8879:1986", - "GlossDef": { - "para": "A meta-markup language, used to create markup languages such as DocBook.", - "GlossSeeAlso": ["GML", "XML"] - }, - "GlossSee": "markup" - } - } - } - } -} diff --git a/tests/fixtures/third_party/cjson/test10.json b/tests/fixtures/third_party/cjson/test10.json deleted file mode 100644 index d19eb8b..0000000 --- a/tests/fixtures/third_party/cjson/test10.json +++ /dev/null @@ -1 +0,0 @@ -["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"] diff --git a/tests/fixtures/third_party/cjson/test11.json b/tests/fixtures/third_party/cjson/test11.json deleted file mode 100644 index 039c61b..0000000 --- a/tests/fixtures/third_party/cjson/test11.json +++ /dev/null @@ -1,8 +0,0 @@ -{ -"name": "Jack (\"Bee\") Nimble", -"format": {"type": "rect", -"width": 1920, -"height": 1080, -"interlace": false,"frame rate": 24 -} -} diff --git a/tests/fixtures/third_party/cjson/test2.json b/tests/fixtures/third_party/cjson/test2.json deleted file mode 100644 index 5600991..0000000 --- a/tests/fixtures/third_party/cjson/test2.json +++ /dev/null @@ -1,11 +0,0 @@ -{"menu": { - "id": "file", - "value": "File", - "popup": { - "menuitem": [ - {"value": "New", "onclick": "CreateNewDoc()"}, - {"value": "Open", "onclick": "OpenDoc()"}, - {"value": "Close", "onclick": "CloseDoc()"} - ] - } -}} diff --git a/tests/fixtures/third_party/cjson/test9.json b/tests/fixtures/third_party/cjson/test9.json deleted file mode 100644 index 2a939b9..0000000 --- a/tests/fixtures/third_party/cjson/test9.json +++ /dev/null @@ -1,5 +0,0 @@ -[ - [0, -1, 0], - [1, 0, 0], - [0, 0, 1] - ] diff --git a/tests/fixtures/third_party/simdjson/example_config.json b/tests/fixtures/third_party/simdjson/example_config.json deleted file mode 100644 index 25c96f2..0000000 --- a/tests/fixtures/third_party/simdjson/example_config.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "app_name": "MyApp", - "version": "1.0.0", - "port": 8080, - "debug": true, - "features": ["logging", "caching"], - "database": { - "host": "localhost", - "port": 5432 - } -} diff --git a/tests/lua/cjson_compat_spec.lua b/tests/lua/cjson_compat_spec.lua index 643fe1f..aa3c19f 100644 --- a/tests/lua/cjson_compat_spec.lua +++ b/tests/lua/cjson_compat_spec.lua @@ -58,22 +58,31 @@ describe("qjson vs lua-cjson", function() end) local fixture_paths = { - "tests/fixtures/third_party/cjson/test1.json", - "tests/fixtures/third_party/cjson/test2.json", - "tests/fixtures/third_party/cjson/test9.json", - "tests/fixtures/third_party/cjson/test10.json", - "tests/fixtures/third_party/cjson/test11.json", - "tests/fixtures/third_party/simdjson/example_config.json", + "tests/vendor/cJSON/tests/inputs/test1", + "tests/vendor/cJSON/tests/inputs/test2", + "tests/vendor/cJSON/tests/inputs/test3", + "tests/vendor/cJSON/tests/inputs/test4", + "tests/vendor/cJSON/tests/inputs/test5", + "tests/vendor/cJSON/tests/inputs/test7", + "tests/vendor/cJSON/tests/inputs/test8", + "tests/vendor/cJSON/tests/inputs/test9", + "tests/vendor/cJSON/tests/inputs/test10", + "tests/vendor/cJSON/tests/inputs/test11", + "tests/vendor/simdjson/jsonexamples/citm_catalog.json", + "tests/vendor/simdjson/jsonexamples/example_config.json", + "tests/vendor/simdjson/jsonexamples/twitter.json", } for _, path in ipairs(fixture_paths) do - it("materializes like lua-cjson for fixture " .. path, function() - local src = read_file(path) + local p = path + + it("materializes like lua-cjson for fixture " .. p, function() + local src = read_file(p) assert.is_true(deep_equal(qjson.materialize(qjson.decode(src)), cjson.decode(src))) end) - it("encodes a lua-cjson-equivalent value for fixture " .. path, function() - local src = read_file(path) + it("encodes a lua-cjson-equivalent value for fixture " .. p, function() + local src = read_file(p) local out = qjson.encode(qjson.decode(src)) assert.is_true(deep_equal(cjson.decode(out), cjson.decode(src))) end) diff --git a/tests/third_party_fixtures.rs b/tests/third_party_fixtures.rs index bc88b62..59009c0 100644 --- a/tests/third_party_fixtures.rs +++ b/tests/third_party_fixtures.rs @@ -1,4 +1,6 @@ +use std::fs; use std::os::raw::c_int; +use std::path::{Path, PathBuf}; use std::ptr; use qjson::doc::Document; @@ -6,31 +8,9 @@ use qjson::error::qjson_err; use qjson::ffi::*; use qjson::options::{Options, QJSON_MODE_EAGER, QJSON_MODE_LAZY}; -const CJSON_FIXTURES: &[(&str, &[u8])] = &[ - ( - "cjson/test1.json", - include_bytes!("fixtures/third_party/cjson/test1.json"), - ), - ( - "cjson/test2.json", - include_bytes!("fixtures/third_party/cjson/test2.json"), - ), - ( - "cjson/test9.json", - include_bytes!("fixtures/third_party/cjson/test9.json"), - ), - ( - "cjson/test10.json", - include_bytes!("fixtures/third_party/cjson/test10.json"), - ), - ( - "cjson/test11.json", - include_bytes!("fixtures/third_party/cjson/test11.json"), - ), -]; - -const SIMDJSON_EXAMPLE_CONFIG: &[u8] = - include_bytes!("fixtures/third_party/simdjson/example_config.json"); +fn repo_root() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) +} fn parse(s: &[u8]) -> *mut qjson_doc { let mut err: c_int = -1; @@ -40,6 +20,57 @@ fn parse(s: &[u8]) -> *mut qjson_doc { d } +fn parse_file(path: &Path) -> Vec { + fs::read(path).unwrap_or_else(|e| panic!("failed to read {}: {e}", path.display())) +} + +fn assert_parses_in_both_modes(name: &str, data: &[u8]) { + let eager = Options { + mode: QJSON_MODE_EAGER, + max_depth: 0, + }; + let lazy = Options { + mode: QJSON_MODE_LAZY, + max_depth: 0, + }; + + Document::parse_with_options(data, &eager) + .unwrap_or_else(|e| panic!("{name} rejected in eager mode: {e:?}")); + Document::parse_with_options(data, &lazy) + .unwrap_or_else(|e| panic!("{name} rejected in lazy mode: {e:?}")); +} + +fn cjson_input_cases() -> Vec { + let dir = repo_root().join("tests/vendor/cJSON/tests/inputs"); + let mut paths: Vec<_> = fs::read_dir(&dir) + .unwrap_or_else(|e| panic!("missing cJSON submodule at {}: {e}", dir.display())) + .filter_map(Result::ok) + .map(|entry| entry.path()) + .filter(|path| { + let Some(name) = path.file_name().and_then(|s| s.to_str()) else { + return false; + }; + name.starts_with("test") + && !name.ends_with(".expected") + && path.with_file_name(format!("{name}.expected")).is_file() + }) + .collect(); + paths.sort(); + paths +} + +fn simdjson_example_cases() -> Vec { + let dir = repo_root().join("tests/vendor/simdjson/jsonexamples"); + let mut paths: Vec<_> = fs::read_dir(&dir) + .unwrap_or_else(|e| panic!("missing simdjson submodule at {}: {e}", dir.display())) + .filter_map(Result::ok) + .map(|entry| entry.path()) + .filter(|path| path.extension().and_then(|s| s.to_str()) == Some("json")) + .collect(); + paths.sort(); + paths +} + fn get_str(doc: *mut qjson_doc, path: &[u8]) -> String { let mut p: *const u8 = ptr::null(); let mut n: usize = 0; @@ -100,27 +131,58 @@ fn cursor_get_str(cur: &qjson_cursor) -> String { } #[test] -fn cjson_fixtures_parse_in_both_modes() { - let eager = Options { - mode: QJSON_MODE_EAGER, - max_depth: 0, - }; - let lazy = Options { - mode: QJSON_MODE_LAZY, - max_depth: 0, - }; +fn cjson_input_corpus_parses_in_both_modes() { + let cases = cjson_input_cases(); + assert!( + cases.len() >= 10, + "expected the cJSON input corpus, got {} files", + cases.len() + ); - for (name, data) in CJSON_FIXTURES { - Document::parse_with_options(data, &eager) - .unwrap_or_else(|e| panic!("{name} rejected in eager mode: {e:?}")); - Document::parse_with_options(data, &lazy) - .unwrap_or_else(|e| panic!("{name} rejected in lazy mode: {e:?}")); + for path in cases { + let name = path + .strip_prefix(repo_root()) + .unwrap_or(&path) + .display() + .to_string(); + assert_parses_in_both_modes(&name, &parse_file(&path)); + + let expected = path.with_file_name(format!( + "{}.expected", + path.file_name().unwrap().to_string_lossy() + )); + let expected_name = expected + .strip_prefix(repo_root()) + .unwrap_or(&expected) + .display() + .to_string(); + assert_parses_in_both_modes(&expected_name, &parse_file(&expected)); + } +} + +#[test] +fn simdjson_jsonexamples_parse_in_both_modes() { + let cases = simdjson_example_cases(); + assert_eq!( + cases.len(), + 3, + "simdjson jsonexamples currently has three single-document JSON files" + ); + + for path in cases { + let name = path + .strip_prefix(repo_root()) + .unwrap_or(&path) + .display() + .to_string(); + assert_parses_in_both_modes(&name, &parse_file(&path)); } } #[test] fn cjson_nested_object_fixture_paths_are_accessible() { - let doc = parse(include_bytes!("fixtures/third_party/cjson/test1.json")); + let data = parse_file(&repo_root().join("tests/vendor/cJSON/tests/inputs/test1")); + let doc = parse(&data); assert_eq!(get_str(doc, b"glossary.title"), "example glossary"); assert_eq!( @@ -143,7 +205,8 @@ fn cjson_nested_object_fixture_paths_are_accessible() { #[test] fn cjson_menu_and_matrix_fixtures_keep_array_shape() { - let menu_doc = parse(include_bytes!("fixtures/third_party/cjson/test2.json")); + let menu_data = parse_file(&repo_root().join("tests/vendor/cJSON/tests/inputs/test2")); + let menu_doc = parse(&menu_data); assert_eq!(get_str(menu_doc, b"menu.id"), "file"); let items = open(menu_doc, b"menu.popup.menuitem"); @@ -165,7 +228,8 @@ fn cjson_menu_and_matrix_fixtures_keep_array_shape() { ); unsafe { qjson_free(menu_doc) }; - let matrix_doc = parse(include_bytes!("fixtures/third_party/cjson/test9.json")); + let matrix_data = parse_file(&repo_root().join("tests/vendor/cJSON/tests/inputs/test9")); + let matrix_doc = parse(&matrix_data); let root = open(matrix_doc, b""); assert_eq!(len(matrix_doc, b""), 3); let middle_row = cursor_index(&root, 1); @@ -180,7 +244,8 @@ fn cjson_menu_and_matrix_fixtures_keep_array_shape() { #[test] fn cjson_escaped_string_and_spaced_key_fixture_paths_are_accessible() { - let doc = parse(include_bytes!("fixtures/third_party/cjson/test11.json")); + let data = parse_file(&repo_root().join("tests/vendor/cJSON/tests/inputs/test11")); + let doc = parse(&data); assert_eq!(get_str(doc, b"name"), "Jack (\"Bee\") Nimble"); assert_eq!(get_str(doc, b"format.type"), "rect"); @@ -194,7 +259,9 @@ fn cjson_escaped_string_and_spaced_key_fixture_paths_are_accessible() { #[test] fn simdjson_example_config_fixture_paths_are_accessible() { - let doc = parse(SIMDJSON_EXAMPLE_CONFIG); + let data = + parse_file(&repo_root().join("tests/vendor/simdjson/jsonexamples/example_config.json")); + let doc = parse(&data); assert_eq!(get_str(doc, b"app_name"), "MyApp"); assert_eq!(get_str(doc, b"version"), "1.0.0"); @@ -207,6 +274,101 @@ fn simdjson_example_config_fixture_paths_are_accessible() { unsafe { qjson_free(doc) }; } +#[test] +fn simdjson_twitter_fixture_paths_are_accessible() { + let data = parse_file(&repo_root().join("tests/vendor/simdjson/jsonexamples/twitter.json")); + let doc = parse(&data); + + assert_eq!(get_i64(doc, b"search_metadata.count"), 100); + assert_eq!(len(doc, b"statuses"), 100); + + unsafe { qjson_free(doc) }; +} + +#[test] +fn cjson_parser_literals_parse_with_qjson() { + let valid_values = [ + "null", + "true", + "false", + "1.5", + "\"\"", + "\"hello\"", + "[]", + "{}", + "[1, null, true, false, [], \"hello\", {}]", + "{\"one\":1, \"NULL\":null, \"TRUE\":true, \"FALSE\":false, \"array\":[], \"world\":\"hello\", \"object\":{}}", + ]; + + for json in valid_values { + assert_parses_in_both_modes(json, json.as_bytes()); + } +} + +#[test] +fn cjson_number_literals_parse_with_qjson() { + let numbers = [ + "0", + "0.0", + "-0", + "-1", + "-32768", + "-2147483648", + "1", + "32767", + "2147483647", + "0.001", + "10e-10", + "10E-10", + "10e10", + "123e+127", + "123e-128", + "-0.001", + "-10e-10", + "-10E-10", + "-10e20", + "-123e+127", + "-123e-128", + "9999999999999999999999999999999999999999999999912345678901234567", + "9999999999999999999999999999999999999999999999912345678901234567E10", + "999999999999999999999999999999999999999999999991234567890.1234567", + ]; + + for number in numbers { + let json = format!("[{number}]"); + assert_parses_in_both_modes(&json, json.as_bytes()); + } +} + +#[test] +fn cjson_string_literals_decode_with_qjson() { + let cases: &[(&[u8], &[u8])] = &[ + (br#""""#, b""), + ( + br##"" !\"#$%&'()*+,-./\/0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_'abcdefghijklmnopqrstuvwxyz{|}~""##, + b" !\"#$%&'()*+,-.//0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_'abcdefghijklmnopqrstuvwxyz{|}~", + ), + ( + br#""\"\\\/\b\f\n\r\t\u20AC\u732b""#, + b"\"\\/\x08\x0c\n\r\t\xe2\x82\xac\xe7\x8c\xab", + ), + (br#""\uD83D\udc31""#, b"\xf0\x9f\x90\xb1"), + ( + br#""~!@\\#$%^&*()\\\\-\\+{}[]:\\;\\\"\\<\\>?/.,DC=ad,DC=com""#, + b"~!@\\#$%^&*()\\\\-\\+{}[]:\\;\\\"\\<\\>?/.,DC=ad,DC=com", + ), + ]; + + for (json_string, expected) in cases { + let mut json = Vec::from(br#"{"s":"#.as_slice()); + json.extend_from_slice(json_string); + json.extend_from_slice(b"}"); + let doc = parse(&json); + assert_eq!(get_str(doc, b"s").as_bytes(), *expected); + unsafe { qjson_free(doc) }; + } +} + #[test] fn simdjson_big_integer_literals_parse_but_do_not_fit_i64() { let cases = [ diff --git a/tests/vendor/cJSON b/tests/vendor/cJSON new file mode 160000 index 0000000..fb16e5c --- /dev/null +++ b/tests/vendor/cJSON @@ -0,0 +1 @@ +Subproject commit fb16e5cf358798aabb049655975cde8427101056 diff --git a/tests/vendor/simdjson b/tests/vendor/simdjson new file mode 160000 index 0000000..43766fc --- /dev/null +++ b/tests/vendor/simdjson @@ -0,0 +1 @@ +Subproject commit 43766fc7efdedc87111c48bda55f758ca212335b From d530056c64b5576d037667b2cea846a9e213a628 Mon Sep 17 00:00:00 2001 From: Jarvis Date: Mon, 18 May 2026 17:44:24 +0800 Subject: [PATCH 3/7] test: cover all simdjson big integer accesses --- tests/third_party_fixtures.rs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/tests/third_party_fixtures.rs b/tests/third_party_fixtures.rs index 59009c0..5df7836 100644 --- a/tests/third_party_fixtures.rs +++ b/tests/third_party_fixtures.rs @@ -381,9 +381,20 @@ fn simdjson_big_integer_literals_parse_but_do_not_fit_i64() { Document::parse(data).unwrap(); } - let doc = parse(cases[0]); + for data in &cases[..2] { + let doc = parse(data); + let mut v: i64 = 0; + let rc = unsafe { qjson_get_i64(doc, b"val".as_ptr() as *const i8, b"val".len(), &mut v) }; + assert_eq!(rc, qjson_err::QJSON_OUT_OF_RANGE as c_int); + unsafe { qjson_free(doc) }; + } + + let doc = parse(cases[2]); + let root = open(doc, b""); + let big_integer = cursor_index(&root, 1); + let empty = b""; let mut v: i64 = 0; - let rc = unsafe { qjson_get_i64(doc, b"val".as_ptr() as *const i8, b"val".len(), &mut v) }; + let rc = unsafe { qjson_cursor_get_i64(&big_integer, empty.as_ptr() as *const i8, 0, &mut v) }; assert_eq!(rc, qjson_err::QJSON_OUT_OF_RANGE as c_int); unsafe { qjson_free(doc) }; } From 201c732a4408b37f267f9ce4547843a36091f5b1 Mon Sep 17 00:00:00 2001 From: Jarvis Date: Mon, 18 May 2026 17:48:00 +0800 Subject: [PATCH 4/7] test: add negative checks for third-party fixtures --- tests/third_party_fixtures.rs | 94 +++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/tests/third_party_fixtures.rs b/tests/third_party_fixtures.rs index 5df7836..84c0999 100644 --- a/tests/third_party_fixtures.rs +++ b/tests/third_party_fixtures.rs @@ -93,6 +93,13 @@ fn get_bool(doc: *mut qjson_doc, path: &[u8]) -> bool { v != 0 } +fn is_null(doc: *mut qjson_doc, path: &[u8]) -> bool { + let mut v: c_int = -1; + let rc = unsafe { qjson_is_null(doc, path.as_ptr() as *const i8, path.len(), &mut v) }; + assert_eq!(rc, qjson_err::QJSON_OK as c_int); + v != 0 +} + fn len(doc: *mut qjson_doc, path: &[u8]) -> usize { let mut n: usize = 0; let rc = unsafe { qjson_len(doc, path.as_ptr() as *const i8, path.len(), &mut n) }; @@ -305,6 +312,93 @@ fn cjson_parser_literals_parse_with_qjson() { } } +#[test] +fn cjson_empty_and_null_literals_keep_shape() { + let empty_array = parse(b"[]"); + assert_eq!(len(empty_array, b""), 0); + unsafe { qjson_free(empty_array) }; + + let empty_object = parse(b"{}"); + assert_eq!(len(empty_object, b""), 0); + unsafe { qjson_free(empty_object) }; + + let array_with_null = parse(b"[null]"); + assert!(is_null(array_with_null, b"[0]")); + unsafe { qjson_free(array_with_null) }; + + let object_with_null = parse(br#"{"null":null}"#); + assert!(is_null(object_with_null, b"null")); + unsafe { qjson_free(object_with_null) }; +} + +#[test] +fn cjson_malformed_container_literals_are_rejected() { + let invalid = [ + b"".as_slice(), + b"[".as_slice(), + b"]".as_slice(), + b"{".as_slice(), + b"}".as_slice(), + b"[1,]".as_slice(), + br#"{"one"}"#.as_slice(), + br#"{"one":1,}"#.as_slice(), + ]; + + for json in invalid { + assert!( + Document::parse(json).is_err(), + "malformed cJSON parser literal was accepted: {:?}", + String::from_utf8_lossy(json) + ); + } +} + +#[test] +fn cjson_fixture_invalid_paths_and_type_mismatches_fail() { + let data = parse_file(&repo_root().join("tests/vendor/cJSON/tests/inputs/test11")); + let doc = parse(&data); + + let mut wrong_type_p: *const u8 = ptr::null(); + let mut wrong_type_n: usize = 0; + let rc = unsafe { + qjson_get_str( + doc, + b"format.width".as_ptr() as *const i8, + b"format.width".len(), + &mut wrong_type_p, + &mut wrong_type_n, + ) + }; + assert_eq!(rc, qjson_err::QJSON_TYPE_MISMATCH as c_int); + + let mut p: *const u8 = ptr::null(); + let mut n: usize = 0; + let rc = unsafe { + qjson_get_str( + doc, + b"format.missing".as_ptr() as *const i8, + b"format.missing".len(), + &mut p, + &mut n, + ) + }; + assert_eq!(rc, qjson_err::QJSON_NOT_FOUND as c_int); + + let mut nested = std::mem::MaybeUninit::::uninit(); + let format_type = open(doc, b"format.type"); + let rc = unsafe { + qjson_cursor_field( + &format_type, + b"child".as_ptr() as *const i8, + b"child".len(), + nested.as_mut_ptr(), + ) + }; + assert_eq!(rc, qjson_err::QJSON_TYPE_MISMATCH as c_int); + + unsafe { qjson_free(doc) }; +} + #[test] fn cjson_number_literals_parse_with_qjson() { let numbers = [ From af6df1f87f7771f3c0bef67048aa9915657b2be2 Mon Sep 17 00:00:00 2001 From: Jarvis Date: Mon, 18 May 2026 17:50:15 +0800 Subject: [PATCH 5/7] test: relax simdjson example count assertion --- tests/third_party_fixtures.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/third_party_fixtures.rs b/tests/third_party_fixtures.rs index 84c0999..a11f5ba 100644 --- a/tests/third_party_fixtures.rs +++ b/tests/third_party_fixtures.rs @@ -170,10 +170,9 @@ fn cjson_input_corpus_parses_in_both_modes() { #[test] fn simdjson_jsonexamples_parse_in_both_modes() { let cases = simdjson_example_cases(); - assert_eq!( - cases.len(), - 3, - "simdjson jsonexamples currently has three single-document JSON files" + assert!( + !cases.is_empty(), + "expected at least one single-document JSON file in simdjson jsonexamples" ); for path in cases { From f80549e9731d266d9aee8358988d6710edb5c2fa Mon Sep 17 00:00:00 2001 From: Jarvis Date: Mon, 18 May 2026 21:54:01 +0800 Subject: [PATCH 6/7] test: cover remaining upstream JSON fixture files --- tests/fixtures/third_party/README.md | 11 ++-- tests/lua/cjson_compat_spec.lua | 20 +++++++ tests/third_party_fixtures.rs | 82 ++++++++++++++++++++++++++++ 3 files changed, 109 insertions(+), 4 deletions(-) diff --git a/tests/fixtures/third_party/README.md b/tests/fixtures/third_party/README.md index a5719ab..7e50241 100644 --- a/tests/fixtures/third_party/README.md +++ b/tests/fixtures/third_party/README.md @@ -4,11 +4,14 @@ qjson reuses mature upstream JSON test data through git submodules instead of copying large C/C++ test harnesses into this repository. - `tests/vendor/cJSON`: `DaveGamble/cJSON`, MIT licensed. Rust and Lua tests - consume `tests/inputs/*` fixtures with matching `.expected` files, and Rust - ports selected parser literals from cJSON number/string/array tests. + consume every `tests/inputs/test*` JSON fixture with matching `.expected` + files, the JSON files from `tests/json-patch-tests/`, and Rust ports parser + literals from cJSON number/string/array tests. `tests/inputs/test6` is an + upstream HTML error page, so qjson keeps it as a negative parse case. - `tests/vendor/simdjson`: `simdjson/simdjson`, dual Apache-2.0/MIT licensed. - qjson uses the MIT option and consumes the single-document `.json` files in - `jsonexamples/`; the `.ndjson` streaming example is intentionally excluded. + qjson uses the MIT option and consumes every single-document `.json` file in + `jsonexamples/`; the `.ndjson` streaming example is split by line so every + record is parsed as an individual JSON document. The upstream submodules carry their own license files: diff --git a/tests/lua/cjson_compat_spec.lua b/tests/lua/cjson_compat_spec.lua index aa3c19f..443eca7 100644 --- a/tests/lua/cjson_compat_spec.lua +++ b/tests/lua/cjson_compat_spec.lua @@ -31,6 +31,12 @@ local function deep_equal(a, b) return true end +local function assert_equivalent_json(src) + assert.is_true(deep_equal(qjson.materialize(qjson.decode(src)), cjson.decode(src))) + local out = qjson.encode(qjson.decode(src)) + assert.is_true(deep_equal(cjson.decode(out), cjson.decode(src))) +end + describe("qjson vs lua-cjson", function() it("agrees on simple string field", function() local s = '{"a":"x"}' @@ -68,6 +74,10 @@ describe("qjson vs lua-cjson", function() "tests/vendor/cJSON/tests/inputs/test9", "tests/vendor/cJSON/tests/inputs/test10", "tests/vendor/cJSON/tests/inputs/test11", + "tests/vendor/cJSON/tests/json-patch-tests/cjson-utils-tests.json", + "tests/vendor/cJSON/tests/json-patch-tests/package.json", + "tests/vendor/cJSON/tests/json-patch-tests/spec_tests.json", + "tests/vendor/cJSON/tests/json-patch-tests/tests.json", "tests/vendor/simdjson/jsonexamples/citm_catalog.json", "tests/vendor/simdjson/jsonexamples/example_config.json", "tests/vendor/simdjson/jsonexamples/twitter.json", @@ -87,4 +97,14 @@ describe("qjson vs lua-cjson", function() assert.is_true(deep_equal(cjson.decode(out), cjson.decode(src))) end) end + + it("materializes and encodes each simdjson NDJSON record like lua-cjson", function() + local src = read_file("tests/vendor/simdjson/jsonexamples/amazon_cellphones.ndjson") + local records = 0 + for line in src:gmatch("([^\r\n]+)") do + records = records + 1 + assert_equivalent_json(line) + end + assert.is_true(records >= 793) + end) end) diff --git a/tests/third_party_fixtures.rs b/tests/third_party_fixtures.rs index a11f5ba..d2eeee5 100644 --- a/tests/third_party_fixtures.rs +++ b/tests/third_party_fixtures.rs @@ -59,6 +59,18 @@ fn cjson_input_cases() -> Vec { paths } +fn cjson_json_patch_cases() -> Vec { + let dir = repo_root().join("tests/vendor/cJSON/tests/json-patch-tests"); + let mut paths: Vec<_> = fs::read_dir(&dir) + .unwrap_or_else(|e| panic!("missing cJSON json-patch-tests at {}: {e}", dir.display())) + .filter_map(Result::ok) + .map(|entry| entry.path()) + .filter(|path| path.extension().and_then(|s| s.to_str()) == Some("json")) + .collect(); + paths.sort(); + paths +} + fn simdjson_example_cases() -> Vec { let dir = repo_root().join("tests/vendor/simdjson/jsonexamples"); let mut paths: Vec<_> = fs::read_dir(&dir) @@ -71,6 +83,18 @@ fn simdjson_example_cases() -> Vec { paths } +fn simdjson_ndjson_cases() -> Vec { + let dir = repo_root().join("tests/vendor/simdjson/jsonexamples"); + let mut paths: Vec<_> = fs::read_dir(&dir) + .unwrap_or_else(|e| panic!("missing simdjson submodule at {}: {e}", dir.display())) + .filter_map(Result::ok) + .map(|entry| entry.path()) + .filter(|path| path.extension().and_then(|s| s.to_str()) == Some("ndjson")) + .collect(); + paths.sort(); + paths +} + fn get_str(doc: *mut qjson_doc, path: &[u8]) -> String { let mut p: *const u8 = ptr::null(); let mut n: usize = 0; @@ -167,6 +191,35 @@ fn cjson_input_corpus_parses_in_both_modes() { } } +#[test] +fn cjson_json_patch_corpus_parses_in_both_modes() { + let cases = cjson_json_patch_cases(); + assert!( + cases.len() >= 4, + "expected the cJSON json-patch-tests corpus, got {} files", + cases.len() + ); + + for path in cases { + let name = path + .strip_prefix(repo_root()) + .unwrap_or(&path) + .display() + .to_string(); + assert_parses_in_both_modes(&name, &parse_file(&path)); + } +} + +#[test] +fn cjson_non_json_input_is_rejected() { + let path = repo_root().join("tests/vendor/cJSON/tests/inputs/test6"); + let data = parse_file(&path); + assert!( + Document::parse(&data).is_err(), + "cJSON test6 is an HTML error page and must not parse as JSON" + ); +} + #[test] fn simdjson_jsonexamples_parse_in_both_modes() { let cases = simdjson_example_cases(); @@ -185,6 +238,35 @@ fn simdjson_jsonexamples_parse_in_both_modes() { } } +#[test] +fn simdjson_ndjson_examples_parse_each_record_in_both_modes() { + let cases = simdjson_ndjson_cases(); + assert!( + !cases.is_empty(), + "expected at least one simdjson ndjson example file" + ); + + let mut records = 0; + for path in cases { + let data = parse_file(&path); + for (line_no, line) in data.split(|b| *b == b'\n').enumerate() { + let line = line.strip_suffix(b"\r").unwrap_or(line); + if line.is_empty() { + continue; + } + records += 1; + let name = format!( + "{}:{}", + path.strip_prefix(repo_root()).unwrap_or(&path).display(), + line_no + 1 + ); + assert_parses_in_both_modes(&name, line); + } + } + + assert!(records >= 793, "expected simdjson NDJSON records, got {records}"); +} + #[test] fn cjson_nested_object_fixture_paths_are_accessible() { let data = parse_file(&repo_root().join("tests/vendor/cJSON/tests/inputs/test1")); From 925a5b97493be6dab1b9aaa2c6cb6f2a66450908 Mon Sep 17 00:00:00 2001 From: Jarvis Date: Mon, 18 May 2026 22:03:13 +0800 Subject: [PATCH 7/7] test: split lua upstream fixture specs --- tests/lua/cjson_compat_spec.lua | 52 +++++++++++++++++++++------------ 1 file changed, 33 insertions(+), 19 deletions(-) diff --git a/tests/lua/cjson_compat_spec.lua b/tests/lua/cjson_compat_spec.lua index 443eca7..c41c3af 100644 --- a/tests/lua/cjson_compat_spec.lua +++ b/tests/lua/cjson_compat_spec.lua @@ -31,13 +31,35 @@ local function deep_equal(a, b) return true end -local function assert_equivalent_json(src) +local function assert_materializes_like_lua_cjson(src) assert.is_true(deep_equal(qjson.materialize(qjson.decode(src)), cjson.decode(src))) +end + +local function assert_encodes_like_lua_cjson(src) local out = qjson.encode(qjson.decode(src)) assert.is_true(deep_equal(cjson.decode(out), cjson.decode(src))) end -describe("qjson vs lua-cjson", function() +local function assert_equivalent_json(src) + assert_materializes_like_lua_cjson(src) + assert_encodes_like_lua_cjson(src) +end + +local function assert_fixture_paths(paths) + for _, path in ipairs(paths) do + local p = path + + it("materializes like lua-cjson for fixture " .. p, function() + assert_materializes_like_lua_cjson(read_file(p)) + end) + + it("encodes a lua-cjson-equivalent value for fixture " .. p, function() + assert_encodes_like_lua_cjson(read_file(p)) + end) + end +end + +describe("qjson lua-cjson compatibility smoke", function() it("agrees on simple string field", function() local s = '{"a":"x"}' assert.are.equal(cjson.decode(s).a, qjson.parse(s):get_str("a")) @@ -62,8 +84,10 @@ describe("qjson vs lua-cjson", function() local s = '{"body":{"model":"gpt"}}' assert.are.equal(cjson.decode(s).body.model, qjson.parse(s):get_str("body.model")) end) +end) - local fixture_paths = { +describe("qjson cJSON upstream fixtures", function() + assert_fixture_paths({ "tests/vendor/cJSON/tests/inputs/test1", "tests/vendor/cJSON/tests/inputs/test2", "tests/vendor/cJSON/tests/inputs/test3", @@ -78,25 +102,15 @@ describe("qjson vs lua-cjson", function() "tests/vendor/cJSON/tests/json-patch-tests/package.json", "tests/vendor/cJSON/tests/json-patch-tests/spec_tests.json", "tests/vendor/cJSON/tests/json-patch-tests/tests.json", + }) +end) + +describe("qjson simdjson upstream fixtures", function() + assert_fixture_paths({ "tests/vendor/simdjson/jsonexamples/citm_catalog.json", "tests/vendor/simdjson/jsonexamples/example_config.json", "tests/vendor/simdjson/jsonexamples/twitter.json", - } - - for _, path in ipairs(fixture_paths) do - local p = path - - it("materializes like lua-cjson for fixture " .. p, function() - local src = read_file(p) - assert.is_true(deep_equal(qjson.materialize(qjson.decode(src)), cjson.decode(src))) - end) - - it("encodes a lua-cjson-equivalent value for fixture " .. p, function() - local src = read_file(p) - local out = qjson.encode(qjson.decode(src)) - assert.is_true(deep_equal(cjson.decode(out), cjson.decode(src))) - end) - end + }) it("materializes and encodes each simdjson NDJSON record like lua-cjson", function() local src = read_file("tests/vendor/simdjson/jsonexamples/amazon_cellphones.ndjson")