Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ once_cell = "1"

[dev-dependencies]
proptest = "1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"

[profile.release]
opt-level = 3
Expand Down
37 changes: 36 additions & 1 deletion benches/lua_bench.lua
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package.path = package.path .. ";./lua/?.lua"
package.path = package.path .. ";./lua/?.lua;./benches/?.lua"
package.cpath = package.cpath .. ";./target/release/lib?.so"

local qjson = require("qjson")
local cjson = require("cjson")
local manifest_mod = require("manifest")
local simdjson_ok, simdjson_or_err = pcall(function()
return require("resty.simdjson").new()
end)
Expand Down Expand Up @@ -955,6 +956,40 @@ local function run_benchmarks()
end)
end
end -- filter == "interleaved"

-- Manifest-driven scenarios: reuse the real-world fixture manifest shared
-- with the Rust correctness gate (tests/fixtures/manifest.json). Fixture
-- paths, per-fixture iteration counts and access paths all come from the
-- manifest, so the benchmark and the correctness tests cover the same
-- payloads and paths. The access closure is generated from each fixture's
-- declared checks rather than being hand-written here. NDJSON fixtures are
-- correctness-only (validated by the Rust gate) and skipped here.
if not filter or filter == "manifest" then
print("=== manifest fixtures ===")

local manifest = manifest_mod.load()
for _, f in ipairs(manifest.fixtures) do
if manifest_mod.has_ci(f, "bench") and f.format == "json" then
local payload = read_file(f.path)
local access = manifest_mod.qjson_access(f.checks)
local iters = f.bench_iters or 500
print(string.format("--- %s [%s] (%d bytes) ---",
f.id, f.payload_type, #payload))

bench("qjson.parse + manifest access", iters, function()
local d = qjson.parse(payload)
access(d)
end)

if has_pooled_api then
bench("qjson pooled :parse + manifest access", iters, function()
local d = pooled_decoder:parse(payload)
access(d)
end)
end
end
end
end -- filter == "manifest"
end

if smoke_mode then
Expand Down
68 changes: 68 additions & 0 deletions benches/manifest.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
-- Shared loader for the real-world fixture manifest (issue #139).
--
-- The manifest at tests/fixtures/manifest.json is the single source of truth
-- for fixture paths, payload classes, access paths and expected values. The
-- Rust correctness gate (tests/manifest_fixtures.rs) and this Lua benchmark
-- helper both read it, so paths and scenarios live in exactly one place.

local cjson = require("cjson")

local M = {}

local function read_file(p)
local f = assert(io.open(p, "rb"))
local s = f:read("*a")
f:close()
return s
end

-- Decode the manifest. `path` is repo-root relative; the bench is launched from
-- the repository root (see the Makefile `bench` target).
function M.load(path)
return cjson.decode(read_file(path or "tests/fixtures/manifest.json"))
end

-- Index fixtures by id for direct lookup.
function M.by_id(manifest)
local m = {}
for _, f in ipairs(manifest.fixtures) do
m[f.id] = f
end
return m
end

-- True if `fixture.ci` contains `tag` (pr | scheduled | bench).
function M.has_ci(fixture, tag)
for _, c in ipairs(fixture.ci or {}) do
if c == tag then return true end
end
return false
end

-- Build an access function over a qjson Doc that touches every check path with
-- the getter matching its declared type. This is the manifest-driven
-- replacement for hand-written per-fixture access closures.
function M.qjson_access(checks)
return function(d)
for i = 1, #checks do
local c = checks[i]
local t = c.type
if t == "string" then
local _ = d:get_str(c.path)
elseif t == "number" then
local _ = d:get_f64(c.path)
elseif t == "bool" then
local _ = d:get_bool(c.path)
elseif t == "null" then
local _ = d:is_null(c.path)
else -- object | array
local _ = d:typeof(c.path)
end
if c.len ~= nil then
local _ = d:len(c.path)
end
end
end
end

return M
107 changes: 107 additions & 0 deletions tests/fixtures/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# Real-world fixture manifest

`manifest.json` is the single source of truth for qjson's real-world test
corpus. Both the Rust correctness gate (`tests/manifest_fixtures.rs`) and the
Lua benchmark harness (`benches/lua_bench.lua` via `benches/manifest.lua`) read
it, so fixture paths, access paths and expected values are declared in exactly
one place instead of being duplicated across the two suites.

Originating issue: [#139](https://github.com/api7/lua-qjson/issues/139).

## Format

`manifest.json` is plain JSON so both consumers can parse it with tools they
already have — `serde_json` (a Rust dev-dependency) on the Rust side and
`lua-cjson` on the Lua side. The manifest is deliberately **not** parsed with
qjson itself, to avoid a bug in qjson breaking the harness that is supposed to
catch it.

All `path` values are **repo-root relative**. Both consumers run from the
repository root (`CARGO_MANIFEST_DIR` for Rust, the `Makefile` working directory
for Lua).

## Schema

```jsonc
{
"version": 1,
"fixtures": [
{
"id": "rest_api_small", // unique identifier
"path": "benches/fixtures/small_api.json",
"source": "qjson bench fixture", // human-readable origin
"payload_type": "rest_api", // rest_api | unicode_heavy | wide_object
// | deep_nesting | ndjson
"format": "json", // json | ndjson
"size_bytes": 2115, // informational
"structural_density": "medium", // optional: low | medium | high
"workloads": ["parse_access", "decode_access",
"decode_encode", "modify_encode"],
"ci": ["pr", "scheduled", "bench"], // where the fixture is exercised
"bench_iters": 5000, // optional: benchmark iteration count
"checks": [ /* see below */ ]
}
]
}
```

### Check entries

Each check declares one access path and its expected result as a
`type` + optional `value` + optional `len` triple:

```jsonc
{ "path": "info.version", "type": "string", "value": "1.0.0" }
{ "path": "messages", "type": "array", "len": 4 }
{ "record": 2, "path": "[1]", "type": "string", "value": "Motorola" }
```

- `path` — qjson path syntax: dotted keys plus `[i]` indices
(e.g. `choices[0].finish_reason`). The empty string `""` addresses the
document root (useful for scalar roots and for `len` on the top-level
container).
- `type` — one of `string | number | bool | null | object | array`. Always
checked via `qjson_typeof`.
- `value` — optional expected value for scalars (`string` / `number` / `bool`).
Omitted for `null` and containers. See the number note below.
- `len` — optional. For `string` it is the decoded **byte** length; for
`object` / `array` it is the member / element count.
- `record` — **NDJSON only**: 0-based line index selecting which record the
`path` is resolved against.

### Numbers

`value` for a `number` check is stored as a JSON number and compared as `f64`
with exact equality. Pick fixture values that are exactly representable as
`f64` (integers, short decimals like `0.5`). Avoid values such as `2.9` that
have no exact binary representation; check the type only, or assert on a
neighbouring exact field instead.

### NDJSON

A fixture with `"format": "ndjson"` is split on `\n` (a trailing `\r` is
trimmed) and each non-empty line is parsed as an independent qjson document.
Checks select a line with `record` (default `0`) and resolve `path` within that
record. NDJSON fixtures are correctness-only; the Lua latency benchmark skips
them.

## Adding a fixture

1. Prefer reusing an existing corpus file: `benches/fixtures/*`,
`tests/vendor/simdjson/jsonexamples/*`, `tests/vendor/cJSON/tests/*`, or a
`JSONTestSuite` sample. Only add a new file under `tests/fixtures/data/` when
no existing payload fits (e.g. the generated `wide_object.json` /
`deep_nesting.json`).
2. Add a fixture entry to `manifest.json` with its `id`, `path`,
`payload_type`, `format`, `ci`, `workloads` and at least one `check`.
3. Add checks for the key access paths you want guarded, using values read from
the actual file.
4. Run the Rust gate:

```sh
cargo test --release --test manifest_fixtures
```

It fails loudly if a path, type, value or length does not match the file.
5. To exercise the fixture in the benchmark, include `"bench"` in `ci` and set
`bench_iters`; `make bench` (or `... lua_bench.lua manifest`) will pick it up.
1 change: 1 addition & 0 deletions tests/fixtures/data/deep_nesting.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[42]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]
1 change: 1 addition & 0 deletions tests/fixtures/data/wide_object.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"k00000":0,"k00001":2,"k00002":4,"k00003":6,"k00004":8,"k00005":10,"k00006":12,"k00007":14,"k00008":16,"k00009":18,"k00010":20,"k00011":22,"k00012":24,"k00013":26,"k00014":28,"k00015":30,"k00016":32,"k00017":34,"k00018":36,"k00019":38,"k00020":40,"k00021":42,"k00022":44,"k00023":46,"k00024":48,"k00025":50,"k00026":52,"k00027":54,"k00028":56,"k00029":58,"k00030":60,"k00031":62,"k00032":64,"k00033":66,"k00034":68,"k00035":70,"k00036":72,"k00037":74,"k00038":76,"k00039":78,"k00040":80,"k00041":82,"k00042":84,"k00043":86,"k00044":88,"k00045":90,"k00046":92,"k00047":94,"k00048":96,"k00049":98,"k00050":100,"k00051":102,"k00052":104,"k00053":106,"k00054":108,"k00055":110,"k00056":112,"k00057":114,"k00058":116,"k00059":118,"k00060":120,"k00061":122,"k00062":124,"k00063":126,"k00064":128,"k00065":130,"k00066":132,"k00067":134,"k00068":136,"k00069":138,"k00070":140,"k00071":142,"k00072":144,"k00073":146,"k00074":148,"k00075":150,"k00076":152,"k00077":154,"k00078":156,"k00079":158,"k00080":160,"k00081":162,"k00082":164,"k00083":166,"k00084":168,"k00085":170,"k00086":172,"k00087":174,"k00088":176,"k00089":178,"k00090":180,"k00091":182,"k00092":184,"k00093":186,"k00094":188,"k00095":190,"k00096":192,"k00097":194,"k00098":196,"k00099":198,"k00100":200,"k00101":202,"k00102":204,"k00103":206,"k00104":208,"k00105":210,"k00106":212,"k00107":214,"k00108":216,"k00109":218,"k00110":220,"k00111":222,"k00112":224,"k00113":226,"k00114":228,"k00115":230,"k00116":232,"k00117":234,"k00118":236,"k00119":238,"k00120":240,"k00121":242,"k00122":244,"k00123":246,"k00124":248,"k00125":250,"k00126":252,"k00127":254,"k00128":256,"k00129":258,"k00130":260,"k00131":262,"k00132":264,"k00133":266,"k00134":268,"k00135":270,"k00136":272,"k00137":274,"k00138":276,"k00139":278,"k00140":280,"k00141":282,"k00142":284,"k00143":286,"k00144":288,"k00145":290,"k00146":292,"k00147":294,"k00148":296,"k00149":298,"k00150":300,"k00151":302,"k00152":304,"k00153":306,"k00154":308,"k00155":310,"k00156":312,"k00157":314,"k00158":316,"k00159":318,"k00160":320,"k00161":322,"k00162":324,"k00163":326,"k00164":328,"k00165":330,"k00166":332,"k00167":334,"k00168":336,"k00169":338,"k00170":340,"k00171":342,"k00172":344,"k00173":346,"k00174":348,"k00175":350,"k00176":352,"k00177":354,"k00178":356,"k00179":358,"k00180":360,"k00181":362,"k00182":364,"k00183":366,"k00184":368,"k00185":370,"k00186":372,"k00187":374,"k00188":376,"k00189":378,"k00190":380,"k00191":382,"k00192":384,"k00193":386,"k00194":388,"k00195":390,"k00196":392,"k00197":394,"k00198":396,"k00199":398,"k00200":400,"k00201":402,"k00202":404,"k00203":406,"k00204":408,"k00205":410,"k00206":412,"k00207":414,"k00208":416,"k00209":418,"k00210":420,"k00211":422,"k00212":424,"k00213":426,"k00214":428,"k00215":430,"k00216":432,"k00217":434,"k00218":436,"k00219":438,"k00220":440,"k00221":442,"k00222":444,"k00223":446,"k00224":448,"k00225":450,"k00226":452,"k00227":454,"k00228":456,"k00229":458,"k00230":460,"k00231":462,"k00232":464,"k00233":466,"k00234":468,"k00235":470,"k00236":472,"k00237":474,"k00238":476,"k00239":478,"k00240":480,"k00241":482,"k00242":484,"k00243":486,"k00244":488,"k00245":490,"k00246":492,"k00247":494,"k00248":496,"k00249":498,"k00250":500,"k00251":502,"k00252":504,"k00253":506,"k00254":508,"k00255":510}
119 changes: 119 additions & 0 deletions tests/fixtures/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
{
"version": 1,
"fixtures": [
{
"id": "rest_api_small",
"path": "benches/fixtures/small_api.json",
"source": "qjson bench fixture",
"payload_type": "rest_api",
"format": "json",
"size_bytes": 2115,
"structural_density": "medium",
"workloads": ["parse_access", "decode_access", "decode_encode", "modify_encode"],
"ci": ["pr", "scheduled", "bench"],
"bench_iters": 5000,
"checks": [
{ "path": "model", "type": "string", "value": "gpt-4" },
{ "path": "stream", "type": "bool", "value": false },
{ "path": "max_tokens", "type": "number", "value": 1024 },
{ "path": "messages", "type": "array", "len": 4 },
{ "path": "messages[0].role", "type": "string", "value": "system" },
{ "path": "metadata.tags", "type": "array", "len": 4 },
{ "path": "metadata.user_preferences.code_blocks", "type": "bool", "value": true }
]
},
{
"id": "rest_api_medium",
"path": "benches/fixtures/medium_resp.json",
"source": "qjson bench fixture",
"payload_type": "rest_api",
"format": "json",
"size_bytes": 60368,
"structural_density": "low",
"workloads": ["parse_access", "decode_access", "modify_encode"],
"ci": ["scheduled", "bench"],
"bench_iters": 500,
"checks": [
{ "path": "id", "type": "string", "value": "resp_2026051599999" },
{ "path": "object", "type": "string", "value": "chat.completion" },
{ "path": "created", "type": "number", "value": 1747353600 },
{ "path": "choices", "type": "array", "len": 1 },
{ "path": "choices[0].finish_reason", "type": "string", "value": "stop" },
{ "path": "usage.total_tokens", "type": "number", "value": 1750 }
]
},
{
"id": "unicode_heavy_twitter",
"path": "tests/vendor/simdjson/jsonexamples/twitter.json",
"source": "simdjson jsonexamples",
"payload_type": "unicode_heavy",
"format": "json",
"size_bytes": 631515,
"structural_density": "high",
"workloads": ["parse_access", "decode_access"],
"ci": ["scheduled", "bench"],
"bench_iters": 200,
"checks": [
{ "path": "statuses", "type": "array", "len": 100 },
{ "path": "statuses[0].lang", "type": "string", "value": "ja" },
{ "path": "statuses[0].user.screen_name", "type": "string", "value": "ayuu0123" },
{ "path": "search_metadata.count", "type": "number", "value": 100 }
]
},
{
"id": "wide_object",
"path": "tests/fixtures/data/wide_object.json",
"source": "qjson generated fixture (256 keys k00000..k00255, value i*2)",
"payload_type": "wide_object",
"format": "json",
"size_bytes": 3275,
"structural_density": "high",
"workloads": ["parse_access"],
"ci": ["pr", "bench"],
"bench_iters": 5000,
"checks": [
{ "path": "", "type": "object", "len": 256 },
{ "path": "k00000", "type": "number", "value": 0 },
{ "path": "k00128", "type": "number", "value": 256 },
{ "path": "k00255", "type": "number", "value": 510 }
]
},
{
"id": "deep_nesting",
"path": "tests/fixtures/data/deep_nesting.json",
"source": "qjson generated fixture (256 nested arrays, leaf 42)",
"payload_type": "deep_nesting",
"format": "json",
"size_bytes": 515,
"structural_density": "high",
"workloads": ["parse_access"],
"ci": ["pr"],
"checks": [
{ "path": "", "type": "array", "len": 1 },
{ "path": "[0]", "type": "array", "len": 1 },
{ "path": "[0][0][0][0][0]", "type": "array", "len": 1 }
]
},
{
"id": "ndjson_amazon",
"path": "tests/vendor/simdjson/jsonexamples/amazon_cellphones.ndjson",
"source": "simdjson jsonexamples",
"payload_type": "ndjson",
"format": "ndjson",
"size_bytes": 277673,
"structural_density": "medium",
"workloads": ["parse_access"],
"ci": ["scheduled", "bench"],
"bench_iters": 100,
"checks": [
{ "record": 0, "path": "", "type": "array", "len": 9 },
{ "record": 0, "path": "[0]", "type": "string", "value": "asin" },
{ "record": 1, "path": "[0]", "type": "string", "value": "B0000SX2UC" },
{ "record": 1, "path": "[1]", "type": "string", "value": "Nokia" },
{ "record": 1, "path": "[7]", "type": "number", "value": 14 },
{ "record": 2, "path": "[1]", "type": "string", "value": "Motorola" },
{ "record": 2, "path": "[8]", "type": "string", "value": "$49.95" }
]
}
]
}
Loading
Loading