mirror of
https://github.com/nlohmann/json.git
synced 2026-10-26 15:58:55 +00:00
Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eed5792dbd | ||
|
|
d4faace387 | ||
|
|
165f91ed3c | ||
|
|
7833865dbd | ||
|
|
9707519b24 | ||
|
|
2d040c89d0 | ||
|
|
182b31f902 | ||
|
|
6775994761 | ||
|
|
70d1baa57a | ||
|
|
536672e927 | ||
|
|
55cdc93ff8 | ||
|
|
db5bd7fbcc | ||
|
|
89bd5d63e1 | ||
|
|
3fb7d9c710 | ||
|
|
fa8134ae85 |
No files matched your search
File diff suppressed because it is too large.
Load diff
@@ -255,6 +255,81 @@ struct number_token
|
|||||||
bool is_float; ///< a fraction or an exponent
|
bool is_float; ///< a fraction or an exponent
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// advance over ASCII digits, eight bytes at a time
|
||||||
|
inline const unsigned char* digit_run(const unsigned char* p, const unsigned char* e) noexcept
|
||||||
|
{
|
||||||
|
constexpr std::uint64_t ones = 0x0101010101010101ull;
|
||||||
|
while (e - p >= 8)
|
||||||
|
{
|
||||||
|
// (the digits become 0..9; a byte above 9, or with bit 7 set, is no digit. A carry
|
||||||
|
// out of a byte with bit 7 set only affects the bytes above, after the first one
|
||||||
|
// that count_trailing_zeros finds.)
|
||||||
|
const std::uint64_t x = read_eight_bytes(p) ^ (ones * 0x30u);
|
||||||
|
const std::uint64_t not_digit = ((x + (ones * 0x76u)) | x) & (ones * 0x80u);
|
||||||
|
if (not_digit != 0)
|
||||||
|
{
|
||||||
|
return p + (static_cast<std::size_t>(count_trailing_zeros(not_digit)) / 8);
|
||||||
|
}
|
||||||
|
p += 8;
|
||||||
|
}
|
||||||
|
while (p != e && is_digit(*p))
|
||||||
|
{
|
||||||
|
++p;
|
||||||
|
}
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// whether the n bytes at p are ASCII digits (n > 0)
|
||||||
|
inline bool all_digits(const unsigned char* p, std::size_t n) noexcept
|
||||||
|
{
|
||||||
|
const auto digits = [](const unsigned char* q)
|
||||||
|
{
|
||||||
|
constexpr std::uint64_t ones = 0x0101010101010101ull;
|
||||||
|
const std::uint64_t x = read_eight_bytes(q) ^ (ones * 0x30u);
|
||||||
|
return (((x + (ones * 0x76u)) | x) & (ones * 0x80u)) == 0;
|
||||||
|
};
|
||||||
|
if (n >= 8)
|
||||||
|
{
|
||||||
|
for (std::size_t i = 0; i + 8 < n; i += 8)
|
||||||
|
{
|
||||||
|
if (!digits(p + i))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return digits(p + n - 8); // (overlapping the bytes before: they are digits)
|
||||||
|
}
|
||||||
|
for (std::size_t i = 0; i < n; ++i)
|
||||||
|
{
|
||||||
|
if (!is_digit(p[i]))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether a float token has the shape that the parser records for most of
|
||||||
|
/// them: [-]digits.digits, with the numbers of digits of the node's layout
|
||||||
|
/// (extra). Then the token is a number of that layout, and the general scan
|
||||||
|
/// that follows is not needed.
|
||||||
|
inline bool plain_float_token(const unsigned char* s, std::size_t len, std::uint16_t extra) noexcept
|
||||||
|
{
|
||||||
|
const std::size_t int_digits = extra & 0xFFu;
|
||||||
|
const std::size_t frac_digits = extra >> 8u;
|
||||||
|
if (int_digits == 0 || int_digits >= 255 || frac_digits == 0 || frac_digits >= 255 || len == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const std::size_t negative = *s == '-' ? 1 : 0;
|
||||||
|
if (len != negative + int_digits + 1 + frac_digits)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const unsigned char* const p = s + negative;
|
||||||
|
return !(int_digits > 1 && *p == '0') && p[int_digits] == '.' && all_digits(p, int_digits) && all_digits(p + int_digits + 1, frac_digits);
|
||||||
|
}
|
||||||
|
|
||||||
/// whether [s, s + len) is a JSON number (the grammar the parser accepts)
|
/// whether [s, s + len) is a JSON number (the grammar the parser accepts)
|
||||||
inline bool scan_number_token(const unsigned char* s, std::size_t len, number_token& t)
|
inline bool scan_number_token(const unsigned char* s, std::size_t len, number_token& t)
|
||||||
{
|
{
|
||||||
@@ -273,11 +348,7 @@ inline bool scan_number_token(const unsigned char* s, std::size_t len, number_to
|
|||||||
}
|
}
|
||||||
else if (*p >= '1' && *p <= '9')
|
else if (*p >= '1' && *p <= '9')
|
||||||
{
|
{
|
||||||
do
|
p = digit_run(p + 1, e);
|
||||||
{
|
|
||||||
++p;
|
|
||||||
}
|
|
||||||
while (p != e && is_digit(*p));
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -289,10 +360,7 @@ inline bool scan_number_token(const unsigned char* s, std::size_t len, number_to
|
|||||||
if (p != e && *p == '.')
|
if (p != e && *p == '.')
|
||||||
{
|
{
|
||||||
const unsigned char* const f0 = ++p;
|
const unsigned char* const f0 = ++p;
|
||||||
while (p != e && is_digit(*p))
|
p = digit_run(p, e);
|
||||||
{
|
|
||||||
++p;
|
|
||||||
}
|
|
||||||
if (p == f0)
|
if (p == f0)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
@@ -403,10 +471,15 @@ struct float_range
|
|||||||
template<typename Check>
|
template<typename Check>
|
||||||
bool check_distinct_ranges(std::vector<byte_range>& ranges, Check check)
|
bool check_distinct_ranges(std::vector<byte_range>& ranges, Check check)
|
||||||
{
|
{
|
||||||
std::sort(ranges.begin(), ranges.end(), [](const byte_range & a, const byte_range & b)
|
// (save() writes the ranges in the order of the nodes, which is sorted already)
|
||||||
|
const auto before = [](const byte_range & a, const byte_range & b)
|
||||||
{
|
{
|
||||||
return a.off != b.off ? a.off < b.off : a.len < b.len;
|
return a.off != b.off ? a.off < b.off : a.len < b.len;
|
||||||
});
|
};
|
||||||
|
if (!std::is_sorted(ranges.begin(), ranges.end(), before))
|
||||||
|
{
|
||||||
|
std::sort(ranges.begin(), ranges.end(), before);
|
||||||
|
}
|
||||||
std::size_t end = 0;
|
std::size_t end = 0;
|
||||||
for (std::size_t i = 0; i < ranges.size(); ++i)
|
for (std::size_t i = 0; i < ranges.size(); ++i)
|
||||||
{
|
{
|
||||||
@@ -428,7 +501,8 @@ bool check_distinct_ranges(std::vector<byte_range>& ranges, Check check)
|
|||||||
/// record layouts that match it), tokens must not overlap.
|
/// record layouts that match it), tokens must not overlap.
|
||||||
inline bool check_float_ranges(std::vector<float_range>& ranges, const unsigned char* text)
|
inline bool check_float_ranges(std::vector<float_range>& ranges, const unsigned char* text)
|
||||||
{
|
{
|
||||||
std::sort(ranges.begin(), ranges.end(), [](const float_range & a, const float_range & b)
|
// (save() writes the tokens in the order of the nodes, which is sorted already)
|
||||||
|
const auto before = [](const float_range & a, const float_range & b)
|
||||||
{
|
{
|
||||||
if (a.off != b.off)
|
if (a.off != b.off)
|
||||||
{
|
{
|
||||||
@@ -439,7 +513,11 @@ inline bool check_float_ranges(std::vector<float_range>& ranges, const unsigned
|
|||||||
return a.len < b.len;
|
return a.len < b.len;
|
||||||
}
|
}
|
||||||
return a.extra < b.extra;
|
return a.extra < b.extra;
|
||||||
});
|
};
|
||||||
|
if (!std::is_sorted(ranges.begin(), ranges.end(), before))
|
||||||
|
{
|
||||||
|
std::sort(ranges.begin(), ranges.end(), before);
|
||||||
|
}
|
||||||
std::size_t end = 0;
|
std::size_t end = 0;
|
||||||
std::size_t i = 0;
|
std::size_t i = 0;
|
||||||
while (i < ranges.size())
|
while (i < ranges.size())
|
||||||
@@ -449,14 +527,18 @@ inline bool check_float_ranges(std::vector<float_range>& ranges, const unsigned
|
|||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
number_token t{};
|
|
||||||
const unsigned char* const s = text + r.off;
|
const unsigned char* const s = text + r.off;
|
||||||
|
std::uint16_t layout = r.extra;
|
||||||
|
if (!plain_float_token(s, r.len, r.extra))
|
||||||
|
{
|
||||||
|
number_token t{};
|
||||||
if (!scan_number_token(s, r.len, t) || !float_token_finite(s, r.len, t))
|
if (!scan_number_token(s, r.len, t) || !float_token_finite(s, r.len, t))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// the digit layout the parser records (or "many", as compaction writes it)
|
// the digit layout the parser records (or "many", as compaction writes it)
|
||||||
const std::uint16_t layout = float_layout(t);
|
layout = float_layout(t);
|
||||||
|
}
|
||||||
for (; i < ranges.size() && ranges[i].off == r.off && ranges[i].len == r.len; ++i)
|
for (; i < ranges.size() && ranges[i].off == r.off && ranges[i].len == r.len; ++i)
|
||||||
{
|
{
|
||||||
if (ranges[i].extra != layout && ranges[i].extra != 0xFFFFu)
|
if (ranges[i].extra != layout && ranges[i].extra != 0xFFFFu)
|
||||||
|
|||||||
@@ -743,8 +743,12 @@ class basic_json_view
|
|||||||
template<typename, bool> friend class basic_json_document;
|
template<typename, bool> friend class basic_json_document;
|
||||||
template<typename, bool> friend class basic_json_view;
|
template<typename, bool> friend class basic_json_view;
|
||||||
template<typename, typename> friend class detail::view::editor;
|
template<typename, typename> friend class detail::view::editor;
|
||||||
|
template<typename, typename> friend class detail::view::view_side;
|
||||||
friend iterator;
|
friend iterator;
|
||||||
|
|
||||||
|
/// whether the view is one of an editable document
|
||||||
|
static constexpr bool is_editable = Editable;
|
||||||
|
|
||||||
basic_json_view(const document_data* d, const node* n) noexcept
|
basic_json_view(const document_data* d, const node* n) noexcept
|
||||||
: m_doc(d), m_node(n)
|
: m_doc(d), m_node(n)
|
||||||
{}
|
{}
|
||||||
|
|||||||
Generated
+1386
-434
File diff suppressed because it is too large.
Load diff
@@ -21,10 +21,6 @@ python3 tests/benchmarks/json_view/compare.py --data <json_test_data directory>
|
|||||||
- The other libraries come from the system: pkg-config, or Homebrew (`brew install yyjson simdjson boost`). With
|
- The other libraries come from the system: pkg-config, or Homebrew (`brew install yyjson simdjson boost`). With
|
||||||
`--download`, pinned releases are downloaded instead and checked against their SHA-256. Without Boost headers (or
|
`--download`, pinned releases are downloaded instead and checked against their SHA-256. Without Boost headers (or
|
||||||
with `--no-boost`), the Boost.JSON columns are skipped, and the results say so.
|
with `--no-boost`), the Boost.JSON columns are skipped, and the results say so.
|
||||||
- `--aligned` additionally builds and runs everything with all functions aligned to 64 bytes (`-mllvm
|
|
||||||
-align-all-functions=6` for Clang, `-falign-functions=64 -falign-loops=64` for GCC), and reports both result
|
|
||||||
tables as "default build" and "aligned build". Code layout alone can move a result by a few percent; a difference
|
|
||||||
that appears in only one of the two tables is a layout artifact. Off by default, which doubles the run time.
|
|
||||||
- `--corpus file...` adds files to the corpus benchmark, e.g. those of
|
- `--corpus file...` adds files to the corpus benchmark, e.g. those of
|
||||||
[simdjson-data](https://github.com/simdjson/simdjson-data) or the
|
[simdjson-data](https://github.com/simdjson/simdjson-data) or the
|
||||||
[yyjson benchmark](https://github.com/ibireme/yyjson_benchmark).
|
[yyjson benchmark](https://github.com/ibireme/yyjson_benchmark).
|
||||||
@@ -43,7 +39,7 @@ where `json_view` stands on another architecture; they are not meant for publica
|
|||||||
|
|
||||||
## What is measured
|
## What is measured
|
||||||
|
|
||||||
`bench_view.cpp` runs these workloads on twitter, citm_catalog, canada, jeopardy, a single tweet (`status`), and a
|
`bench_view.cpp` runs four workloads on twitter, citm_catalog, canada, jeopardy, a single tweet (`status`), and a
|
||||||
JSON-RPC request (`rpc`):
|
JSON-RPC request (`rpc`):
|
||||||
|
|
||||||
| workload | what it does |
|
| workload | what it does |
|
||||||
@@ -52,7 +48,6 @@ JSON-RPC request (`rpc`):
|
|||||||
| traverse | parse, then visit every value, convert every number, touch every string and key |
|
| traverse | parse, then visit every value, convert every number, touch every string and key |
|
||||||
| select | parse, then read a few fields per record (e.g. id, user name, and retweet count of each tweet) |
|
| select | parse, then read a few fields per record (e.g. id, user name, and retweet count of each tweet) |
|
||||||
| dump | serialize a parsed document (compact) |
|
| dump | serialize a parsed document (compact) |
|
||||||
| parse+dump | build a fresh document, then serialize it (compact), in one timed call |
|
|
||||||
|
|
||||||
`bench_corpus.cpp` runs parse, traverse, and dump on any list of files, so that no library is tuned to a handful of
|
`bench_corpus.cpp` runs parse, traverse, and dump on any list of files, so that no library is tuned to a handful of
|
||||||
documents. Its dump also writes the numbers as they are in the input: `json_view` with `number_format::source`, and
|
documents. Its dump also writes the numbers as they are in the input: `json_view` with `number_format::source`, and
|
||||||
@@ -71,32 +66,11 @@ call follows an untimed call of the same engine: otherwise the engine after `jso
|
|||||||
cleaning up the tens of thousands of nodes `json::parse` just freed (with glibc, this made `json_view` look 1.7 times
|
cleaning up the tens of thousands of nodes `json::parse` just freed (with glibc, this made `json_view` look 1.7 times
|
||||||
slower on citm_catalog traverse).
|
slower on citm_catalog traverse).
|
||||||
|
|
||||||
`json_view` is an index into the source text, not a tree of owning nodes. Its like-for-like peers are yyjson and
|
|
||||||
simdjson DOM (immutable, indexed, no per-node allocation). Boost.JSON and `json::parse` build mutable, owning DOMs and
|
|
||||||
are reference points for what that costs, not equals; simdjson On-Demand is lazy and forward-only, so it is the other
|
|
||||||
way round: it has no random access to pay for. All engines validate UTF-8 and syntax by default.
|
|
||||||
|
|
||||||
Number conversion differs: `json_view` converts integers while parsing but converts floating-point numbers lazily,
|
|
||||||
when they are read or dumped; yyjson (default flags) and simdjson DOM convert every number while parsing. The cost of
|
|
||||||
float conversion therefore appears in the traverse and dump rows for `json_view`, and in the parse row for the others.
|
|
||||||
For float-heavy documents (canada), the fair reading is a sum of parse and use: the `traverse` workload already
|
|
||||||
includes the parse, and `parse+dump` measures a fresh document and its dump in one timed call; the `dump` row alone
|
|
||||||
leaves the conversion in `json_view` and out of the others. Eager conversion at parse time was prototyped as an opt-in
|
|
||||||
and measured: canada traverse (with its parse) was 4-7 % faster, but parse+dump 11-30 % slower and parse 36-90 % slower. It was
|
|
||||||
dropped, so lazy conversion is the only mode.
|
|
||||||
|
|
||||||
`json_view` can also be edited: `json_editable_document` has the same read API, and the `(editable)` rows in
|
|
||||||
`bench_view` measure it on the same workloads. They are the fair counterpart of Boost.JSON and `json::parse`, which are
|
|
||||||
mutable too; the plain `json_view` rows are the counterpart of yyjson and simdjson DOM, which are not. `bench_edit` adds
|
|
||||||
the read-modify-write workloads. Read the tables with that in mind: an immutable-only library is not compared with an
|
|
||||||
editable `json_view` unless it says so.
|
|
||||||
|
|
||||||
The engines do not all offer the same features, which the numbers should be read with:
|
The engines do not all offer the same features, which the numbers should be read with:
|
||||||
|
|
||||||
| engine | document | random access | editable | notes |
|
| engine | document | random access | editable | notes |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| `json_view` | immutable index into the text | yes | no | a fresh document per parse; "reused" parses into the same document |
|
| `json_view` | immutable index into the text | yes | no | a fresh document per parse; "reused" parses into the same document |
|
||||||
| `json_view` (editable) | index into the text plus edits | yes | yes, in place | `json_editable_document`; keeps member order and number spellings |
|
|
||||||
| yyjson | immutable (`yyjson_read`) | yes | via a mutable copy | |
|
| yyjson | immutable (`yyjson_read`) | yes | via a mutable copy | |
|
||||||
| simdjson DOM | immutable, parser reused | yes | no | "fresh" uses a new parser per parse |
|
| simdjson DOM | immutable, parser reused | yes | no | "fresh" uses a new parser per parse |
|
||||||
| simdjson On-Demand | none: forward-only, lazy | no | no | only traverse and select |
|
| simdjson On-Demand | none: forward-only, lazy | no | no | only traverse and select |
|
||||||
|
|||||||
@@ -8,8 +8,7 @@
|
|||||||
|
|
||||||
// Same-feature-set benchmark: read-only JSON documents with random access.
|
// Same-feature-set benchmark: read-only JSON documents with random access.
|
||||||
//
|
//
|
||||||
// json_view nlohmann/json_view.hpp (fresh document per parse / reused;
|
// json_view nlohmann/json_view.hpp (fresh document per parse / reused)
|
||||||
// "(editable)": json_editable_document, same read API)
|
|
||||||
// yyjson yyjson_read(): immutable document, random access
|
// yyjson yyjson_read(): immutable document, random access
|
||||||
// simdjson DOM dom::parser (reused, as recommended; "fresh": a new parser
|
// simdjson DOM dom::parser (reused, as recommended; "fresh": a new parser
|
||||||
// per parse): immutable, random access
|
// per parse): immutable, random access
|
||||||
@@ -20,11 +19,7 @@
|
|||||||
//
|
//
|
||||||
// Workloads: parse (build + free), traverse (visit everything, convert every
|
// Workloads: parse (build + free), traverse (visit everything, convert every
|
||||||
// number, touch every string and key), select (a few fields per document),
|
// number, touch every string and key), select (a few fields per document),
|
||||||
// dump (compact serialization of the parsed document), and parse+dump (a fresh
|
// dump (compact serialization of the parsed document).
|
||||||
// document, then its dump, in one timed call). json_view converts floating-point
|
|
||||||
// numbers lazily (when read or dumped), yyjson and simdjson DOM while parsing:
|
|
||||||
// for float-heavy documents, traverse (which includes the parse) and parse+dump
|
|
||||||
// are the fair reading of the parse and dump rows.
|
|
||||||
// All engines run interleaved in every round, each timed call after an untimed
|
// All engines run interleaved in every round, each timed call after an untimed
|
||||||
// one of the same engine; the best round is reported.
|
// one of the same engine; the best round is reported.
|
||||||
#include <nlohmann/json_view.hpp>
|
#include <nlohmann/json_view.hpp>
|
||||||
@@ -48,8 +43,6 @@
|
|||||||
|
|
||||||
using nlohmann::json;
|
using nlohmann::json;
|
||||||
using nlohmann::json_document;
|
using nlohmann::json_document;
|
||||||
using nlohmann::json_editable_document;
|
|
||||||
using nlohmann::json_editable_view;
|
|
||||||
using nlohmann::json_view;
|
using nlohmann::json_view;
|
||||||
|
|
||||||
static volatile double g_sink;
|
static volatile double g_sink;
|
||||||
@@ -62,8 +55,7 @@ struct stats
|
|||||||
|
|
||||||
// ---------------- traversal ----------------
|
// ---------------- traversal ----------------
|
||||||
|
|
||||||
template<class V>
|
static void walk(json_view v, stats& st)
|
||||||
static void walk_view(V v, stats& st)
|
|
||||||
{
|
{
|
||||||
++st.nodes;
|
++st.nodes;
|
||||||
switch (v.type())
|
switch (v.type())
|
||||||
@@ -72,42 +64,32 @@ static void walk_view(V v, stats& st)
|
|||||||
for (auto it = v.begin(); it != v.end(); ++it)
|
for (auto it = v.begin(); it != v.end(); ++it)
|
||||||
{
|
{
|
||||||
st.str += it.key().size();
|
st.str += it.key().size();
|
||||||
walk_view<V>(*it, st);
|
walk(*it, st);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case json::value_t::array:
|
case json::value_t::array:
|
||||||
for (const V e : v)
|
for (const json_view e : v)
|
||||||
{
|
{
|
||||||
walk_view<V>(e, st);
|
walk(e, st);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case json::value_t::string:
|
case json::value_t::string:
|
||||||
st.str += v.get_string().size();
|
st.str += v.get_string().size();
|
||||||
break;
|
break;
|
||||||
case json::value_t::number_integer:
|
case json::value_t::number_integer:
|
||||||
st.num += static_cast<double>(v.template get<std::int64_t>());
|
st.num += static_cast<double>(v.get<std::int64_t>());
|
||||||
break;
|
break;
|
||||||
case json::value_t::number_unsigned:
|
case json::value_t::number_unsigned:
|
||||||
st.num += static_cast<double>(v.template get<std::uint64_t>());
|
st.num += static_cast<double>(v.get<std::uint64_t>());
|
||||||
break;
|
break;
|
||||||
case json::value_t::number_float:
|
case json::value_t::number_float:
|
||||||
st.num += v.template get<double>();
|
st.num += v.get<double>();
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void walk(json_view v, stats& st)
|
|
||||||
{
|
|
||||||
walk_view<json_view>(v, st);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void walk(json_editable_view v, stats& st)
|
|
||||||
{
|
|
||||||
walk_view<json_editable_view>(v, st);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void walk(const json& j, stats& st)
|
static void walk(const json& j, stats& st)
|
||||||
{
|
{
|
||||||
++st.nodes;
|
++st.nodes;
|
||||||
@@ -318,21 +300,20 @@ static void walk(const boost::json::value& v, stats& st)
|
|||||||
// status (one tweet): id, user.screen_name, retweet_count
|
// status (one tweet): id, user.screen_name, retweet_count
|
||||||
// rpc: method, params.minuend, id
|
// rpc: method, params.minuend, id
|
||||||
|
|
||||||
template<class V>
|
static double pick(const std::string& name, json_view r)
|
||||||
static double pick_view(const std::string& name, V r)
|
|
||||||
{
|
{
|
||||||
double acc = 0;
|
double acc = 0;
|
||||||
if (name == "twitter" || name == "status")
|
if (name == "twitter" || name == "status")
|
||||||
{
|
{
|
||||||
auto one = [&](V s)
|
auto one = [&](json_view s)
|
||||||
{
|
{
|
||||||
acc += static_cast<double>(s["id"].template get<std::uint64_t>());
|
acc += static_cast<double>(s["id"].get<std::uint64_t>());
|
||||||
acc += static_cast<double>(s["user"]["screen_name"].get_string().size());
|
acc += static_cast<double>(s["user"]["screen_name"].get_string().size());
|
||||||
acc += static_cast<double>(s["retweet_count"].template get<std::int64_t>());
|
acc += static_cast<double>(s["retweet_count"].get<std::int64_t>());
|
||||||
};
|
};
|
||||||
if (name == "twitter")
|
if (name == "twitter")
|
||||||
{
|
{
|
||||||
for (const V s : r["statuses"])
|
for (const json_view s : r["statuses"])
|
||||||
{
|
{
|
||||||
one(s);
|
one(s);
|
||||||
}
|
}
|
||||||
@@ -344,20 +325,20 @@ static double pick_view(const std::string& name, V r)
|
|||||||
}
|
}
|
||||||
else if (name == "citm_catalog")
|
else if (name == "citm_catalog")
|
||||||
{
|
{
|
||||||
for (const V p : r["performances"])
|
for (const json_view p : r["performances"])
|
||||||
{
|
{
|
||||||
acc += static_cast<double>(p["id"].template get<std::uint64_t>() + p["eventId"].template get<std::uint64_t>() + p["seatCategories"].size());
|
acc += static_cast<double>(p["id"].get<std::uint64_t>() + p["eventId"].get<std::uint64_t>() + p["seatCategories"].size());
|
||||||
}
|
}
|
||||||
acc += static_cast<double>(r["events"].size());
|
acc += static_cast<double>(r["events"].size());
|
||||||
}
|
}
|
||||||
else if (name == "canada")
|
else if (name == "canada")
|
||||||
{
|
{
|
||||||
const V g = r["features"][0]["geometry"];
|
const json_view g = r["features"][0]["geometry"];
|
||||||
acc += static_cast<double>(r["type"].get_string().size() + g["type"].get_string().size() + g["coordinates"].size());
|
acc += static_cast<double>(r["type"].get_string().size() + g["type"].get_string().size() + g["coordinates"].size());
|
||||||
}
|
}
|
||||||
else if (name == "jeopardy")
|
else if (name == "jeopardy")
|
||||||
{
|
{
|
||||||
for (const V q : r)
|
for (const json_view q : r)
|
||||||
{
|
{
|
||||||
acc += q["round"].get_string() == "Final Jeopardy!" ? 1 : 0;
|
acc += q["round"].get_string() == "Final Jeopardy!" ? 1 : 0;
|
||||||
acc += static_cast<double>(q["category"].get_string().size());
|
acc += static_cast<double>(q["category"].get_string().size());
|
||||||
@@ -366,21 +347,11 @@ static double pick_view(const std::string& name, V r)
|
|||||||
else if (name == "rpc")
|
else if (name == "rpc")
|
||||||
{
|
{
|
||||||
acc += static_cast<double>(r["method"].get_string().size());
|
acc += static_cast<double>(r["method"].get_string().size());
|
||||||
acc += static_cast<double>(r["params"]["minuend"].template get<std::int64_t>() + r["id"].template get<std::int64_t>());
|
acc += static_cast<double>(r["params"]["minuend"].get<std::int64_t>() + r["id"].get<std::int64_t>());
|
||||||
}
|
}
|
||||||
return acc;
|
return acc;
|
||||||
}
|
}
|
||||||
|
|
||||||
static double pick(const std::string& name, json_view r)
|
|
||||||
{
|
|
||||||
return pick_view<json_view>(name, r);
|
|
||||||
}
|
|
||||||
|
|
||||||
static double pick(const std::string& name, json_editable_view r)
|
|
||||||
{
|
|
||||||
return pick_view<json_editable_view>(name, r);
|
|
||||||
}
|
|
||||||
|
|
||||||
static double pick(const std::string& name, const json& r)
|
static double pick(const std::string& name, const json& r)
|
||||||
{
|
{
|
||||||
double acc = 0;
|
double acc = 0;
|
||||||
@@ -663,35 +634,15 @@ int main(int argc, char** argv)
|
|||||||
walk(json::parse(dc.text), a);
|
walk(json::parse(dc.text), a);
|
||||||
auto d = json_document::parse(dc.text);
|
auto d = json_document::parse(dc.text);
|
||||||
walk(d.root(), b);
|
walk(d.root(), b);
|
||||||
auto ed = json_editable_document::parse(dc.text);
|
|
||||||
stats e;
|
|
||||||
walk(ed.root(), e);
|
|
||||||
yyjson_doc* y = yyjson_read(dc.text.data(), dc.text.size(), 0);
|
yyjson_doc* y = yyjson_read(dc.text.data(), dc.text.size(), 0);
|
||||||
walk(yyjson_doc_get_root(y), c);
|
walk(yyjson_doc_get_root(y), c);
|
||||||
simdjson::dom::parser p;
|
simdjson::dom::parser p;
|
||||||
walk(p.parse(dc.text).value(), dd);
|
walk(p.parse(dc.text).value(), dd);
|
||||||
const bool ok = a.nodes == b.nodes && a.nodes == e.nodes && a.str == e.str && std::fabs(a.num - e.num) <= 1e-9 * std::fabs(a.num) && a.nodes == c.nodes && a.nodes == dd.nodes && a.str == b.str && a.str == c.str && a.str == dd.str
|
const bool ok = a.nodes == b.nodes && a.nodes == c.nodes && a.nodes == dd.nodes && a.str == b.str && a.str == c.str && a.str == dd.str
|
||||||
&& std::fabs(a.num - b.num) <= 1e-9 * std::fabs(a.num) && std::fabs(a.num - c.num) <= 1e-9 * std::fabs(a.num);
|
&& std::fabs(a.num - b.num) <= 1e-9 * std::fabs(a.num) && std::fabs(a.num - c.num) <= 1e-9 * std::fabs(a.num);
|
||||||
const double pa = pick(dc.name, json::parse(dc.text)), pb = pick(dc.name, d.root()), pe = pick(dc.name, ed.root()), pc = pick(dc.name, yyjson_doc_get_root(y)), pd = pick(dc.name, p.parse(dc.text).value());
|
const double pa = pick(dc.name, json::parse(dc.text)), pb = pick(dc.name, d.root()), pc = pick(dc.name, yyjson_doc_get_root(y)), pd = pick(dc.name, p.parse(dc.text).value());
|
||||||
std::printf("check %-13s traverse %s select %s\n", dc.name.c_str(), ok ? "OK" : "MISMATCH", (pa == pb && pa == pe && pa == pc && pa == pd) ? "OK" : "MISMATCH");
|
std::printf("check %-13s traverse %s select %s\n", dc.name.c_str(), ok ? "OK" : "MISMATCH", (pa == pb && pa == pc && pa == pd) ? "OK" : "MISMATCH");
|
||||||
yyjson_doc_free(y);
|
yyjson_doc_free(y);
|
||||||
|
|
||||||
// the dumps of the engines may format floats differently, so only: every dump is non-empty, and the
|
|
||||||
// dump of json_view (also editable) and of json::parse parse back to the same number of nodes
|
|
||||||
const std::string dj = json::parse(dc.text).dump();
|
|
||||||
const std::string dv = d.root().dump();
|
|
||||||
const std::string de = ed.root().dump();
|
|
||||||
std::size_t yn = 0;
|
|
||||||
yyjson_doc* y2 = yyjson_read(dc.text.data(), dc.text.size(), 0);
|
|
||||||
char* yo = yyjson_write(y2, 0, &yn);
|
|
||||||
const bool dumps_ok = !dj.empty() && !dv.empty() && !de.empty() && yo != nullptr && yn > 0 && !simdjson::to_string(p.parse(dc.text).value()).empty();
|
|
||||||
std::free(yo);
|
|
||||||
yyjson_doc_free(y2);
|
|
||||||
stats sj_nodes, sv_nodes, se_nodes;
|
|
||||||
walk(json::parse(dj), sj_nodes);
|
|
||||||
walk(json::parse(dv), sv_nodes);
|
|
||||||
walk(json::parse(de), se_nodes);
|
|
||||||
std::printf("check %-13s dump %s\n", dc.name.c_str(), (dumps_ok && sj_nodes.nodes == a.nodes && sv_nodes.nodes == a.nodes && se_nodes.nodes == a.nodes) ? "OK" : "MISMATCH");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::FILE* csv = std::fopen("bench_view.csv", "w");
|
std::FILE* csv = std::fopen("bench_view.csv", "w");
|
||||||
@@ -718,7 +669,6 @@ int main(int argc, char** argv)
|
|||||||
workloads.push_back({"parse", {
|
workloads.push_back({"parse", {
|
||||||
{"json_view", [&] { auto d = json_document::parse(s); g_sink = static_cast<double>(d.node_count()); }},
|
{"json_view", [&] { auto d = json_document::parse(s); g_sink = static_cast<double>(d.node_count()); }},
|
||||||
{"json_view (reused)", [&] { reused.read(s); g_sink = static_cast<double>(reused.node_count()); }},
|
{"json_view (reused)", [&] { reused.read(s); g_sink = static_cast<double>(reused.node_count()); }},
|
||||||
{"json_view (editable)", [&] { auto d = json_editable_document::parse(s); g_sink = static_cast<double>(d.root().size()); }},
|
|
||||||
{"yyjson", [&] { yyjson_doc* d = yyjson_read(s.data(), s.size(), 0); g_sink = static_cast<double>(yyjson_doc_get_val_count(d)); yyjson_doc_free(d); }},
|
{"yyjson", [&] { yyjson_doc* d = yyjson_read(s.data(), s.size(), 0); g_sink = static_cast<double>(yyjson_doc_get_val_count(d)); yyjson_doc_free(d); }},
|
||||||
{"simdjson DOM", [&] { auto e = sj.parse(ps).value_unsafe(); g_sink = e.is_object(); }},
|
{"simdjson DOM", [&] { auto e = sj.parse(ps).value_unsafe(); g_sink = e.is_object(); }},
|
||||||
{"simdjson DOM (fresh)", [&] { simdjson::dom::parser p; auto e = p.parse(ps).value_unsafe(); g_sink = e.is_object(); }},
|
{"simdjson DOM (fresh)", [&] { simdjson::dom::parser p; auto e = p.parse(ps).value_unsafe(); g_sink = e.is_object(); }},
|
||||||
@@ -730,7 +680,6 @@ int main(int argc, char** argv)
|
|||||||
workloads.push_back({"traverse", {
|
workloads.push_back({"traverse", {
|
||||||
{"json_view", [&] { auto d = json_document::parse(s); stats st; walk(d.root(), st); g_sink = st.num; }},
|
{"json_view", [&] { auto d = json_document::parse(s); stats st; walk(d.root(), st); g_sink = st.num; }},
|
||||||
{"json_view (reused)", [&] { reused.read(s); stats st; walk(reused.root(), st); g_sink = st.num; }},
|
{"json_view (reused)", [&] { reused.read(s); stats st; walk(reused.root(), st); g_sink = st.num; }},
|
||||||
{"json_view (editable)", [&] { auto d = json_editable_document::parse(s); stats st; walk(d.root(), st); g_sink = st.num; }},
|
|
||||||
{"yyjson", [&] { yyjson_doc* d = yyjson_read(s.data(), s.size(), 0); stats st; walk(yyjson_doc_get_root(d), st); g_sink = st.num; yyjson_doc_free(d); }},
|
{"yyjson", [&] { yyjson_doc* d = yyjson_read(s.data(), s.size(), 0); stats st; walk(yyjson_doc_get_root(d), st); g_sink = st.num; yyjson_doc_free(d); }},
|
||||||
{"simdjson DOM", [&] { stats st; walk(sj.parse(ps).value_unsafe(), st); g_sink = st.num; }},
|
{"simdjson DOM", [&] { stats st; walk(sj.parse(ps).value_unsafe(), st); g_sink = st.num; }},
|
||||||
{"simdjson OD", [&] { auto d = od.iterate(ps).value_unsafe(); stats st; walk_od(d.get_value().value_unsafe(), st); g_sink = st.num; }},
|
{"simdjson OD", [&] { auto d = od.iterate(ps).value_unsafe(); stats st; walk_od(d.get_value().value_unsafe(), st); g_sink = st.num; }},
|
||||||
@@ -742,31 +691,15 @@ int main(int argc, char** argv)
|
|||||||
workloads.push_back({"select", {
|
workloads.push_back({"select", {
|
||||||
{"json_view", [&] { auto d = json_document::parse(s); g_sink = pick(name, d.root()); }},
|
{"json_view", [&] { auto d = json_document::parse(s); g_sink = pick(name, d.root()); }},
|
||||||
{"json_view (reused)", [&] { reused.read(s); g_sink = pick(name, reused.root()); }},
|
{"json_view (reused)", [&] { reused.read(s); g_sink = pick(name, reused.root()); }},
|
||||||
{"json_view (editable)", [&] { auto d = json_editable_document::parse(s); g_sink = pick(name, d.root()); }},
|
|
||||||
{"yyjson", [&] { yyjson_doc* d = yyjson_read(s.data(), s.size(), 0); g_sink = pick(name, yyjson_doc_get_root(d)); yyjson_doc_free(d); }},
|
{"yyjson", [&] { yyjson_doc* d = yyjson_read(s.data(), s.size(), 0); g_sink = pick(name, yyjson_doc_get_root(d)); yyjson_doc_free(d); }},
|
||||||
{"simdjson DOM", [&] { g_sink = pick(name, sj.parse(ps).value_unsafe()); }},
|
{"simdjson DOM", [&] { g_sink = pick(name, sj.parse(ps).value_unsafe()); }},
|
||||||
{"simdjson OD", [&] { auto d = od.iterate(ps).value_unsafe(); g_sink = pick_od(name, d); }},
|
{"simdjson OD", [&] { auto d = od.iterate(ps).value_unsafe(); g_sink = pick_od(name, d); }},
|
||||||
{"json::parse", [&] { json j = json::parse(s); g_sink = pick(name, j); }},
|
{"json::parse", [&] { json j = json::parse(s); g_sink = pick(name, j); }},
|
||||||
}});
|
}});
|
||||||
// pipeline: a fresh document, then a dump of it, in one timed call
|
|
||||||
// (simdjson DOM reuses its parser, as in the parse row); the traverse
|
|
||||||
// workload above is already parse+traverse
|
|
||||||
workloads.push_back({"parse+dump", {
|
|
||||||
{"json_view", [&] { auto d = json_document::parse(s); std::string o = d.root().dump(); g_sink = static_cast<double>(o.size()); }},
|
|
||||||
{"json_view (editable)", [&] { auto d = json_editable_document::parse(s); std::string o = d.root().dump(); g_sink = static_cast<double>(o.size()); }},
|
|
||||||
{"yyjson", [&] { yyjson_doc* d = yyjson_read(s.data(), s.size(), 0); std::size_t n = 0; char* o = yyjson_write(d, 0, &n); g_sink = static_cast<double>(n); std::free(o); yyjson_doc_free(d); }},
|
|
||||||
{"simdjson DOM", [&] { std::string o = simdjson::to_string(sj.parse(ps).value_unsafe()); g_sink = static_cast<double>(o.size()); }},
|
|
||||||
#if JSON_VIEW_BENCH_BOOST
|
|
||||||
{"Boost.JSON", [&] { boost::json::monotonic_resource mr; auto v = boost::json::parse(s, &mr); std::string o = boost::json::serialize(v); g_sink = static_cast<double>(o.size()); }},
|
|
||||||
#endif
|
|
||||||
{"json::parse", [&] { json j = json::parse(s); std::string o = j.dump(); g_sink = static_cast<double>(o.size()); }},
|
|
||||||
}});
|
|
||||||
{
|
{
|
||||||
// serialization of an already parsed document
|
// serialization of an already parsed document
|
||||||
static json_document vd;
|
static json_document vd;
|
||||||
vd.read(s);
|
vd.read(s);
|
||||||
static json_editable_document ed = json_editable_document::parse(s);
|
|
||||||
ed = json_editable_document::parse(s);
|
|
||||||
static yyjson_doc* yd = nullptr;
|
static yyjson_doc* yd = nullptr;
|
||||||
if (yd)
|
if (yd)
|
||||||
{
|
{
|
||||||
@@ -780,7 +713,6 @@ int main(int argc, char** argv)
|
|||||||
jd = json::parse(s);
|
jd = json::parse(s);
|
||||||
workloads.push_back({"dump", {
|
workloads.push_back({"dump", {
|
||||||
{"json_view", [&] { std::string o = vd.root().dump(); g_sink = static_cast<double>(o.size()); }},
|
{"json_view", [&] { std::string o = vd.root().dump(); g_sink = static_cast<double>(o.size()); }},
|
||||||
{"json_view (editable)", [&] { std::string o = ed.root().dump(); g_sink = static_cast<double>(o.size()); }},
|
|
||||||
{"yyjson", [&] { std::size_t n = 0; char* o = yyjson_write(yd, 0, &n); g_sink = static_cast<double>(n); std::free(o); }},
|
{"yyjson", [&] { std::size_t n = 0; char* o = yyjson_write(yd, 0, &n); g_sink = static_cast<double>(n); std::free(o); }},
|
||||||
{"simdjson DOM", [&] { std::string o = simdjson::to_string(se); g_sink = static_cast<double>(o.size()); }},
|
{"simdjson DOM", [&] { std::string o = simdjson::to_string(se); g_sink = static_cast<double>(o.size()); }},
|
||||||
{"json::parse", [&] { std::string o = jd.dump(); g_sink = static_cast<double>(o.size()); }},
|
{"json::parse", [&] { std::string o = jd.dump(); g_sink = static_cast<double>(o.size()); }},
|
||||||
@@ -808,7 +740,7 @@ int main(int argc, char** argv)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const double ref = best[0];
|
const double ref = best[0];
|
||||||
std::printf("%-13s %-14s", dc.name.c_str(), wl.first.c_str());
|
std::printf("%-13s %-9s", dc.name.c_str(), wl.first.c_str());
|
||||||
for (std::size_t k = 0; k < wl.second.size(); ++k)
|
for (std::size_t k = 0; k < wl.second.size(); ++k)
|
||||||
{
|
{
|
||||||
const double us = best[k] / 1e3;
|
const double us = best[k] / 1e3;
|
||||||
|
|||||||
@@ -239,8 +239,6 @@ def main():
|
|||||||
ap.add_argument('--download', action='store_true', help='use pinned downloads instead of system libraries')
|
ap.add_argument('--download', action='store_true', help='use pinned downloads instead of system libraries')
|
||||||
ap.add_argument('--no-boost', action='store_true', help='skip Boost.JSON')
|
ap.add_argument('--no-boost', action='store_true', help='skip Boost.JSON')
|
||||||
ap.add_argument('--native', action='store_true', help='compile for this CPU (-march=native / -mcpu=native)')
|
ap.add_argument('--native', action='store_true', help='compile for this CPU (-march=native / -mcpu=native)')
|
||||||
ap.add_argument('--aligned', action='store_true',
|
|
||||||
help='also build and run with all functions (and loops) aligned, to make code layout artifacts visible')
|
|
||||||
ap.add_argument('--rounds', type=int, default=30, help='rounds of bench_view (default: 30)')
|
ap.add_argument('--rounds', type=int, default=30, help='rounds of bench_view (default: 30)')
|
||||||
ap.add_argument('--corpus', nargs='*', default=[], help='more files for bench_corpus')
|
ap.add_argument('--corpus', nargs='*', default=[], help='more files for bench_corpus')
|
||||||
ap.add_argument('--build-dir', default=os.path.join(HERE, 'build'), help='where to build (default: build/ next to this script)')
|
ap.add_argument('--build-dir', default=os.path.join(HERE, 'build'), help='where to build (default: build/ next to this script)')
|
||||||
@@ -284,44 +282,30 @@ def main():
|
|||||||
+ ['-I' + d for d in lib.include])
|
+ ['-I' + d for d in lib.include])
|
||||||
objects.append(obj)
|
objects.append(obj)
|
||||||
|
|
||||||
cxx_version = output(cxx + ['--version'])
|
|
||||||
is_clang = 'clang' in cxx_version.lower()
|
|
||||||
# alignment flags: functions and (gcc) loops on 64-byte boundaries
|
|
||||||
align_flags = ['-mllvm', '-align-all-functions=6'] if is_clang else ['-falign-functions=64', '-falign-loops=64']
|
|
||||||
|
|
||||||
# the builds: the default one and, with --aligned, one with aligned code
|
|
||||||
variants = [('default build', args.build_dir, [])]
|
|
||||||
if args.aligned:
|
|
||||||
variants.append(('aligned build', os.path.join(args.build_dir, 'aligned'), align_flags))
|
|
||||||
|
|
||||||
corpus = [os.path.join(args.data, f) for f in DEFAULT_CORPUS] + args.corpus
|
|
||||||
# label -> {benchmark: output}
|
|
||||||
all_outputs = {}
|
|
||||||
for label, build_dir, extra in variants:
|
|
||||||
os.makedirs(build_dir, exist_ok=True)
|
|
||||||
binaries = {}
|
binaries = {}
|
||||||
for bench in ['bench_view', 'bench_corpus', 'bench_edit']:
|
for bench in ['bench_view', 'bench_corpus', 'bench_edit']:
|
||||||
exe = os.path.join(build_dir, bench)
|
exe = os.path.join(args.build_dir, bench)
|
||||||
run(cxx + flags + extra + include + [os.path.join(HERE, bench + '.cpp')] + objects + link + ['-o', exe])
|
run(cxx + flags + include + [os.path.join(HERE, bench + '.cpp')] + objects + link + ['-o', exe])
|
||||||
binaries[bench] = exe
|
binaries[bench] = exe
|
||||||
|
|
||||||
# run: bench_view on its documents, bench_corpus on those and the given files
|
# run: bench_view on its documents, bench_corpus on those and the given files
|
||||||
|
corpus = [os.path.join(args.data, f) for f in DEFAULT_CORPUS] + args.corpus
|
||||||
outputs = {}
|
outputs = {}
|
||||||
outputs['bench_view'] = run([binaries['bench_view'], args.data, str(args.rounds)], cwd=build_dir,
|
outputs['bench_view'] = run([binaries['bench_view'], args.data, str(args.rounds)], cwd=args.build_dir,
|
||||||
capture_output=True, text=True).stdout
|
capture_output=True, text=True).stdout
|
||||||
outputs['bench_corpus'] = run([binaries['bench_corpus']] + corpus, cwd=build_dir,
|
outputs['bench_corpus'] = run([binaries['bench_corpus']] + corpus, cwd=args.build_dir,
|
||||||
capture_output=True, text=True).stdout
|
capture_output=True, text=True).stdout
|
||||||
outputs['bench_edit'] = run([binaries['bench_edit'], args.data, str(max(1, args.rounds // 2))], cwd=build_dir,
|
outputs['bench_edit'] = run([binaries['bench_edit'], args.data, str(max(1, args.rounds // 2))], cwd=args.build_dir,
|
||||||
capture_output=True, text=True).stdout
|
capture_output=True, text=True).stdout
|
||||||
for name, text in outputs.items():
|
for name, text in outputs.items():
|
||||||
print(text)
|
print(text)
|
||||||
all_outputs[label] = outputs
|
|
||||||
|
|
||||||
# results with their metadata
|
# results with their metadata
|
||||||
now = datetime.datetime.now()
|
now = datetime.datetime.now()
|
||||||
host = re.sub(r'[^A-Za-z0-9-]+', '-', platform.node().split('.')[0]) or 'host'
|
host = re.sub(r'[^A-Za-z0-9-]+', '-', platform.node().split('.')[0]) or 'host'
|
||||||
stem = os.path.join(HERE, 'results', f'{now:%Y-%m-%d}-{host}')
|
stem = os.path.join(HERE, 'results', f'{now:%Y-%m-%d}-{host}')
|
||||||
os.makedirs(os.path.dirname(stem), exist_ok=True)
|
os.makedirs(os.path.dirname(stem), exist_ok=True)
|
||||||
|
cxx_version = output(cxx + ['--version'])
|
||||||
compiler_version = cxx_version.splitlines()[0] if cxx_version else ' '.join(cxx)
|
compiler_version = cxx_version.splitlines()[0] if cxx_version else ' '.join(cxx)
|
||||||
meta = [
|
meta = [
|
||||||
('date', f'{now:%Y-%m-%d %H:%M}'),
|
('date', f'{now:%Y-%m-%d %H:%M}'),
|
||||||
@@ -330,7 +314,6 @@ def main():
|
|||||||
('OS', f'{platform.system()} {platform.release()} ({platform.machine()})'),
|
('OS', f'{platform.system()} {platform.release()} ({platform.machine()})'),
|
||||||
('compiler', compiler_version),
|
('compiler', compiler_version),
|
||||||
('flags', ' '.join(flags)),
|
('flags', ' '.join(flags)),
|
||||||
('aligned results', ('included (flags: ' + ' '.join(align_flags) + ')') if args.aligned else 'not included'),
|
|
||||||
('yyjson', libs['yyjson'].version),
|
('yyjson', libs['yyjson'].version),
|
||||||
('simdjson', libs['simdjson'].version),
|
('simdjson', libs['simdjson'].version),
|
||||||
('Boost.JSON', libs['boost'].version if with_boost else 'skipped (not found)'),
|
('Boost.JSON', libs['boost'].version if with_boost else 'skipped (not found)'),
|
||||||
@@ -343,20 +326,15 @@ def main():
|
|||||||
f.write('| | |\n|---|---|\n')
|
f.write('| | |\n|---|---|\n')
|
||||||
for key, value in meta:
|
for key, value in meta:
|
||||||
f.write(f'| {key} | {value} |\n')
|
f.write(f'| {key} | {value} |\n')
|
||||||
for label, outputs in all_outputs.items():
|
|
||||||
# (without --aligned there is only the default build, and no extra heading level)
|
|
||||||
prefix = f'{label}: ' if args.aligned else ''
|
|
||||||
for name, text in outputs.items():
|
for name, text in outputs.items():
|
||||||
f.write(f'\n## {prefix}{name}\n\n```\n{text.rstrip()}\n```\n')
|
f.write(f'\n## {name}\n\n```\n{text.rstrip()}\n```\n')
|
||||||
with open(stem + '.csv', 'w', encoding='utf-8') as out:
|
with open(stem + '.csv', 'w', encoding='utf-8') as out:
|
||||||
out.write(''.join(f'# {key}: {value}\n' for key, value in meta))
|
out.write(''.join(f'# {key}: {value}\n' for key, value in meta))
|
||||||
for label, build_dir, _ in variants:
|
|
||||||
for name in ['bench_view', 'bench_corpus', 'bench_edit']:
|
for name in ['bench_view', 'bench_corpus', 'bench_edit']:
|
||||||
path = os.path.join(build_dir, name + '.csv')
|
path = os.path.join(args.build_dir, name + '.csv')
|
||||||
if os.path.isfile(path):
|
if os.path.isfile(path):
|
||||||
with open(path, encoding='utf-8') as f:
|
with open(path, encoding='utf-8') as f:
|
||||||
# (the default build keeps its plain section names)
|
out.write(f'# {name}\n' + f.read())
|
||||||
out.write(f'# {name}' + (f' ({label})' if args.aligned else '') + '\n' + f.read())
|
|
||||||
print(f'results: {stem}.md, {stem}.csv')
|
print(f'results: {stem}.md, {stem}.csv')
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,364 @@
|
|||||||
|
// __ _____ _____ _____
|
||||||
|
// __| | __| | | | JSON for Modern C++ (supporting code)
|
||||||
|
// | | |__ | | | | | | version 3.12.0
|
||||||
|
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
||||||
|
//
|
||||||
|
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
#include "doctest_compatibility.h"
|
||||||
|
|
||||||
|
#include <nlohmann/json_view.hpp>
|
||||||
|
using nlohmann::json;
|
||||||
|
using nlohmann::ordered_json;
|
||||||
|
using nlohmann::json_document;
|
||||||
|
using nlohmann::ordered_json_editable_document;
|
||||||
|
using nlohmann::json_view;
|
||||||
|
using nlohmann::ordered_json_document;
|
||||||
|
|
||||||
|
#include "json_view_test_helpers.hpp"
|
||||||
|
using json_view_test::generator;
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
// These tests were split off unit-json_view_dump.cpp, whose object file got too
|
||||||
|
// large for the MinGW assembler ("file too big"). Like that file, this one is
|
||||||
|
// also built with C++17, as it mentions JSON_HAS_CPP_17.
|
||||||
|
|
||||||
|
TEST_CASE("json_view comparison")
|
||||||
|
{
|
||||||
|
SECTION("equality of the values parse() produces")
|
||||||
|
{
|
||||||
|
generator g;
|
||||||
|
std::vector<std::string> texts;
|
||||||
|
for (int i = 0; i < 600; ++i)
|
||||||
|
{
|
||||||
|
std::string text;
|
||||||
|
g.value(text, 0);
|
||||||
|
texts.push_back(text);
|
||||||
|
// the same value written differently: sorted keys, canonical numbers
|
||||||
|
texts.push_back(json::parse(text).dump(1));
|
||||||
|
}
|
||||||
|
for (std::size_t i = 0; i + 2 < texts.size(); ++i)
|
||||||
|
{
|
||||||
|
for (std::size_t k = i; k < i + 3; ++k)
|
||||||
|
{
|
||||||
|
CAPTURE(texts[i])
|
||||||
|
CAPTURE(texts[k])
|
||||||
|
const json_document a = json_document::parse(texts[i]);
|
||||||
|
const json_document b = json_document::parse(texts[k]);
|
||||||
|
const json ja = json::parse(texts[i]);
|
||||||
|
const json jb = json::parse(texts[k]);
|
||||||
|
CHECK((a.root() == b.root()) == (ja == jb));
|
||||||
|
CHECK((a.root() != b.root()) == (ja != jb));
|
||||||
|
CHECK((a.root() == jb) == (ja == jb));
|
||||||
|
CHECK((jb == a.root()) == (ja == jb));
|
||||||
|
CHECK((a.root() != jb) == (ja != jb));
|
||||||
|
CHECK((jb != a.root()) == (ja != jb));
|
||||||
|
|
||||||
|
// ordered_json compares members in order
|
||||||
|
const ordered_json_document oa = ordered_json_document::parse(texts[i]);
|
||||||
|
const ordered_json_document ob = ordered_json_document::parse(texts[k]);
|
||||||
|
const ordered_json oja = ordered_json::parse(texts[i]);
|
||||||
|
const ordered_json ojb = ordered_json::parse(texts[k]);
|
||||||
|
CHECK((oa.root() == ob.root()) == (oja == ojb));
|
||||||
|
CHECK((oa.root() == ojb) == (oja == ojb));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("objects whose first keys differ or repeat")
|
||||||
|
{
|
||||||
|
const std::array<const char*, 17> texts =
|
||||||
|
{
|
||||||
|
{
|
||||||
|
R"({"a":1,"b":2})", R"({"a":1,"c":2})", R"({"b":1,"a":2})", R"({"a":1,"a":2,"b":3})", R"({"a":2,"b":3})",
|
||||||
|
R"({"a":1,"b":1,"a":2})", R"({"a":2,"b":1})", R"({"a":1,"a":2})", R"({"b":1})", R"({"a":1,"b":2,"c":3,"d":4,"e":5})",
|
||||||
|
R"({"a":1,"b":2,"c":3,"d":4,"f":5})", R"({"a":1,"b":2,"c":3,"a":4,"e":5})", R"({"a":4,"b":2,"c":3,"e":5})",
|
||||||
|
R"({"\u0061":1,"b":2})", R"({})", R"([{"a":1,"b":2}])", R"([{"a":1,"c":2}])"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
for (const char* const x : texts)
|
||||||
|
{
|
||||||
|
for (const char* const y : texts)
|
||||||
|
{
|
||||||
|
CAPTURE(x)
|
||||||
|
CAPTURE(y)
|
||||||
|
const ordered_json_document dx = ordered_json_document::parse(x);
|
||||||
|
const ordered_json_document dy = ordered_json_document::parse(y);
|
||||||
|
const ordered_json_editable_document ex = ordered_json_editable_document::parse(x);
|
||||||
|
const ordered_json jx = ordered_json::parse(x);
|
||||||
|
const ordered_json jy = ordered_json::parse(y);
|
||||||
|
CHECK((dx.root() == dy.root()) == (jx == jy));
|
||||||
|
CHECK((dx.root() == jy) == (jx == jy));
|
||||||
|
CHECK((ex.root() == dy.root()) == (jx == jy));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("objects with duplicate keys, small and large")
|
||||||
|
{
|
||||||
|
// n members with keys in descending order; with duplicates, some keys
|
||||||
|
// repeat later (the first after unique ones in key order); in reverse,
|
||||||
|
// the members are in ascending key order
|
||||||
|
const auto object = [](int n, bool duplicates, bool reverse, int value)
|
||||||
|
{
|
||||||
|
std::string s = "{";
|
||||||
|
for (int i = 0; i < n; ++i)
|
||||||
|
{
|
||||||
|
const int k = reverse ? i : n - 1 - i;
|
||||||
|
s += (i != 0 ? ",\"k" : "\"k") + std::to_string(100 + k) + "\":" + std::to_string(value + k);
|
||||||
|
}
|
||||||
|
if (duplicates)
|
||||||
|
{
|
||||||
|
for (int k = n / 2; k < n; k += 3)
|
||||||
|
{
|
||||||
|
s += ",\"k" + std::to_string(100 + k) + "\":" + std::to_string(value + k);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s + "}";
|
||||||
|
};
|
||||||
|
std::vector<std::string> texts;
|
||||||
|
for (const int n :
|
||||||
|
{
|
||||||
|
3, 16, 17, 40
|
||||||
|
})
|
||||||
|
{
|
||||||
|
for (const int flags :
|
||||||
|
{
|
||||||
|
0, 1, 2, 3
|
||||||
|
})
|
||||||
|
{
|
||||||
|
texts.push_back(object(n, (flags & 1) != 0, (flags & 2) != 0, 0));
|
||||||
|
}
|
||||||
|
texts.push_back(object(n, true, false, 1)); // other values
|
||||||
|
}
|
||||||
|
// many objects in one comparison (working memory reused across objects)
|
||||||
|
std::string array = "[";
|
||||||
|
std::string array_other = "[";
|
||||||
|
for (std::size_t i = 0; i < texts.size(); ++i)
|
||||||
|
{
|
||||||
|
array += (i != 0 ? "," : "") + texts[i];
|
||||||
|
array_other += (i != 0 ? "," : "") + texts[texts.size() - 1 - i];
|
||||||
|
}
|
||||||
|
texts.push_back(array + "]");
|
||||||
|
texts.push_back(array_other + "]");
|
||||||
|
for (const std::string& x : texts)
|
||||||
|
{
|
||||||
|
for (const std::string& y : texts)
|
||||||
|
{
|
||||||
|
CAPTURE(x)
|
||||||
|
CAPTURE(y)
|
||||||
|
const ordered_json_document dx = ordered_json_document::parse(x);
|
||||||
|
const ordered_json_document dy = ordered_json_document::parse(y);
|
||||||
|
const ordered_json jx = ordered_json::parse(x);
|
||||||
|
const ordered_json jy = ordered_json::parse(y);
|
||||||
|
CHECK((dx.root() == dy.root()) == (jx == jy));
|
||||||
|
CHECK((dx.root() == jy) == (jx == jy));
|
||||||
|
const json_document ux = json_document::parse(x);
|
||||||
|
const json uy = json::parse(y);
|
||||||
|
CHECK((ux.root() == uy) == (json::parse(x) == uy));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("numbers, duplicate keys, member order")
|
||||||
|
{
|
||||||
|
const auto same = [](const char* x, const char* y)
|
||||||
|
{
|
||||||
|
const json_document dx = json_document::parse(x);
|
||||||
|
const json_document dy = json_document::parse(y);
|
||||||
|
return dx.root() == dy.root();
|
||||||
|
};
|
||||||
|
CHECK(same("1", "1.0"));
|
||||||
|
CHECK(same("[1, -1, 2.5]", "[1.0, -1.0, 25e-1]"));
|
||||||
|
CHECK(!same("1", "1.5"));
|
||||||
|
CHECK(same("18446744073709551615", "18446744073709551615"));
|
||||||
|
CHECK(same(R"({"a": 1, "a": 2})", R"({"a": 2})"));
|
||||||
|
CHECK(!same(R"({"a": 1, "a": 2})", R"({"a": 1})"));
|
||||||
|
CHECK(same(R"({"a": 1, "b": 2})", R"({"b": 2, "a": 1})"));
|
||||||
|
CHECK(!same(R"({"a": 1})", R"({"a": 1, "b": 2})"));
|
||||||
|
CHECK(!same("[1, 2]", "[2, 1]"));
|
||||||
|
CHECK(!same("\"a\"", "\"b\""));
|
||||||
|
CHECK(same("\"\\u00e9\"", "\"\xc3\xa9\""));
|
||||||
|
CHECK(!same("null", "false"));
|
||||||
|
CHECK(!same("[]", "{}"));
|
||||||
|
const ordered_json_document dup = ordered_json_document::parse(R"({"a": 1, "b": 2, "a": 3})");
|
||||||
|
const ordered_json_document last = ordered_json_document::parse(R"({"a": 3, "b": 2})");
|
||||||
|
CHECK(dup.root() == last.root());
|
||||||
|
// (== compares as parse() resolves duplicates, a lookup finds the first)
|
||||||
|
CHECK(dup.root()["a"].materialize() == 1);
|
||||||
|
CHECK(dup.root()["a"] != last.root()["a"]);
|
||||||
|
const ordered_json_document ab = ordered_json_document::parse(R"({"a": 1, "b": 2})");
|
||||||
|
const ordered_json_document ba = ordered_json_document::parse(R"({"b": 2, "a": 1})");
|
||||||
|
CHECK(ab.root() != ba.root());
|
||||||
|
|
||||||
|
// discarded values compare as basic_json's do
|
||||||
|
const json discarded(json::value_t::discarded);
|
||||||
|
CHECK((json_view() == json_view()) == (discarded == discarded)); // NOLINT(readability-container-size-empty): operator== is tested
|
||||||
|
CHECK((json_view() == discarded) == (discarded == discarded)); // NOLINT(readability-container-size-empty)
|
||||||
|
const json_document null_document = json_document::parse("null");
|
||||||
|
CHECK(!(json_view() == null_document.root())); // NOLINT(readability-container-size-empty)
|
||||||
|
CHECK(!(null_document.root() == discarded));
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("values that are the same text, or the same value written differently, at any depth")
|
||||||
|
{
|
||||||
|
// nested arrays and objects around depths where the walk over equal text gives up
|
||||||
|
for (const int depth :
|
||||||
|
{
|
||||||
|
1, 5, 47, 48, 49, 50, 60, 300
|
||||||
|
})
|
||||||
|
{
|
||||||
|
CAPTURE(depth)
|
||||||
|
std::string head;
|
||||||
|
std::string tail;
|
||||||
|
for (int i = 0; i < depth; ++i)
|
||||||
|
{
|
||||||
|
head += i % 2 == 0 ? R"({"k":[1,"s",{"x":true},)" : R"([null,2.5,{"y":)";
|
||||||
|
tail.insert(0, i % 2 == 0 ? "]}" : "}]");
|
||||||
|
}
|
||||||
|
// the same text, one number changed, and one number written differently
|
||||||
|
const auto with_number = [&](const char* number)
|
||||||
|
{
|
||||||
|
std::string result = head;
|
||||||
|
result += number;
|
||||||
|
result += tail;
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
const std::string text = with_number("7");
|
||||||
|
const std::string changed = with_number("8");
|
||||||
|
const std::string spelled = with_number("7.0");
|
||||||
|
const json_document a = json_document::parse(text);
|
||||||
|
const json_document b = json_document::parse(text);
|
||||||
|
const json_document c = json_document::parse(changed);
|
||||||
|
const json_document d = json_document::parse(spelled);
|
||||||
|
CHECK(a.root() == b.root());
|
||||||
|
CHECK(a.root() != c.root());
|
||||||
|
CHECK(c.root() != a.root());
|
||||||
|
CHECK(a.root() == d.root());
|
||||||
|
CHECK(d.root() == a.root());
|
||||||
|
CHECK(a.root() == json::parse(text));
|
||||||
|
CHECK(a.root() != json::parse(changed));
|
||||||
|
CHECK(json::parse(spelled) == a.root());
|
||||||
|
const nlohmann::json_editable_document e = nlohmann::json_editable_document::parse(text);
|
||||||
|
CHECK(a.root() == e.root());
|
||||||
|
CHECK(e.root() == a.root());
|
||||||
|
CHECK(c.root() != e.root());
|
||||||
|
CHECK(e.root() == e.root());
|
||||||
|
const ordered_json_document oa = ordered_json_document::parse(text);
|
||||||
|
const ordered_json_document oc = ordered_json_document::parse(changed);
|
||||||
|
CHECK(oa.root() != oc.root());
|
||||||
|
CHECK(oa.root() == ordered_json::parse(text));
|
||||||
|
CHECK(oc.root() != ordered_json::parse(text));
|
||||||
|
}
|
||||||
|
|
||||||
|
// not the same text, but the same value
|
||||||
|
const auto same = [](const char* x, const char* y)
|
||||||
|
{
|
||||||
|
const json_document dx = json_document::parse(x);
|
||||||
|
const json_document dy = json_document::parse(y);
|
||||||
|
const bool equal = dx.root() == dy.root();
|
||||||
|
CHECK(equal == (json::parse(x) == json::parse(y)));
|
||||||
|
CHECK(equal == (dx.root() == json::parse(y)));
|
||||||
|
return equal;
|
||||||
|
};
|
||||||
|
CHECK(same(R"({"a":1,"a":2})", R"({"a":3,"a":2})"));
|
||||||
|
CHECK(same(R"({"a":1,"b":[1,{"c":2,"c":3}]})", R"({"b":[1,{"c":9,"c":3}],"a":1})"));
|
||||||
|
CHECK(!same(R"({"a":1,"a":2})", R"({"a":2,"a":3})"));
|
||||||
|
CHECK(same("[1.0,-0,0,1e2]", "[1.00,0,-0,100.0]"));
|
||||||
|
CHECK(same(R"(["\u00e9","\n"])", "[\"\xc3\xa9\",\"\\n\"]"));
|
||||||
|
CHECK(!same(R"({"a":[1,2,{"b":null}]})", R"({"a":[1,2,{"b":false}]})"));
|
||||||
|
CHECK(!same(R"({"a":[1,2,{"b":null}]})", R"({"a":[1,2,{"c":null}]})"));
|
||||||
|
CHECK(!same(R"({"a":[1,2,{"b":null}]})", R"({"a":[1,2,{"b":null,"c":1}]})"));
|
||||||
|
|
||||||
|
// edited documents: moved sequences, links, and values of new nodes
|
||||||
|
nlohmann::json_editable_document edited = nlohmann::json_editable_document::parse(R"({"a":[1,2,3],"b":{"c":"d"},"e":0.5})");
|
||||||
|
const json_document plain = json_document::parse(R"({"a":[1,2,3,4],"b":{"c":"x","y":null},"e":0.5,"z":[]})");
|
||||||
|
edited.push_back(edited.root()["a"], 4);
|
||||||
|
edited.set(edited.root()["b"], "c", "x");
|
||||||
|
edited.set(edited.root()["b"], "y", nullptr);
|
||||||
|
edited.set(edited.root(), "z", json::array());
|
||||||
|
CHECK(edited.root() == plain.root());
|
||||||
|
CHECK(plain.root() == edited.root());
|
||||||
|
edited.erase(edited.root()["a"], 0);
|
||||||
|
CHECK(edited.root() != plain.root());
|
||||||
|
CHECK(edited.root()["a"] == json::parse("[2,3,4]"));
|
||||||
|
const json_document tail = json_document::parse("[2,3,4]");
|
||||||
|
CHECK(edited.root()["a"] == tail.root());
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("large documents, with a difference at any place")
|
||||||
|
{
|
||||||
|
const auto make = [](std::size_t count, std::size_t changed, bool swap_members)
|
||||||
|
{
|
||||||
|
std::string text = R"({"b":1,"list":[)";
|
||||||
|
for (std::size_t i = 0; i < count; ++i)
|
||||||
|
{
|
||||||
|
text += (i != 0 ? "," : "");
|
||||||
|
const std::string id = std::to_string(i == changed ? i + 1 : i);
|
||||||
|
if (swap_members && i % 3 == 0)
|
||||||
|
{
|
||||||
|
text += R"({"v":[1.5,"s",{"k":)";
|
||||||
|
text += id;
|
||||||
|
text += R"(}],"id":)";
|
||||||
|
text += id;
|
||||||
|
text += "}";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
text += R"({"id":)";
|
||||||
|
text += id;
|
||||||
|
text += R"(,"v":[1.5,"s",{"k":)";
|
||||||
|
text += id;
|
||||||
|
text += "}]}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return text + R"(],"a":{"y":2,"x":3}})";
|
||||||
|
};
|
||||||
|
const std::size_t count = 3000;
|
||||||
|
for (const std::size_t changed :
|
||||||
|
{
|
||||||
|
count, std::size_t{0}, std::size_t{1}, count / 2, count - 1
|
||||||
|
})
|
||||||
|
{
|
||||||
|
for (const bool swap :
|
||||||
|
{
|
||||||
|
false, true
|
||||||
|
})
|
||||||
|
{
|
||||||
|
CAPTURE(changed)
|
||||||
|
CAPTURE(swap)
|
||||||
|
const std::string text_a = make(count, count, false);
|
||||||
|
const std::string text_b = make(count, changed, swap);
|
||||||
|
const json_document a = json_document::parse(text_a);
|
||||||
|
const json_document b = json_document::parse(text_b);
|
||||||
|
const bool equal = changed == count;
|
||||||
|
CHECK((a.root() == b.root()) == equal);
|
||||||
|
CHECK((b.root() == a.root()) == equal);
|
||||||
|
CHECK((a.root() == json::parse(text_b)) == equal);
|
||||||
|
CHECK((json::parse(text_b) == a.root()) == equal);
|
||||||
|
CHECK((a.root()["list"] == b.root()["list"]) == equal);
|
||||||
|
const ordered_json_document oa = ordered_json_document::parse(text_a);
|
||||||
|
const ordered_json_document ob = ordered_json_document::parse(text_b);
|
||||||
|
CHECK((oa.root() == ob.root()) == (equal && !swap));
|
||||||
|
CHECK((oa.root() == ordered_json::parse(text_b)) == (equal && !swap));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("deep nesting")
|
||||||
|
{
|
||||||
|
const std::string deep = std::string(100000, '[') + std::string(100000, ']');
|
||||||
|
const json_document a = json_document::parse(deep);
|
||||||
|
const json_document b = json_document::parse(deep);
|
||||||
|
CHECK(a.root() == b.root());
|
||||||
|
CHECK(a.root() == json::parse(deep));
|
||||||
|
const std::string other = std::string(100000, '[') + "1" + std::string(100000, ']');
|
||||||
|
const json_document c = json_document::parse(other);
|
||||||
|
CHECK(a.root() != c.root());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -432,101 +432,6 @@ TEST_CASE("json_view dump")
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("json_view comparison")
|
|
||||||
{
|
|
||||||
SECTION("equality of the values parse() produces")
|
|
||||||
{
|
|
||||||
generator g;
|
|
||||||
std::vector<std::string> texts;
|
|
||||||
for (int i = 0; i < 600; ++i)
|
|
||||||
{
|
|
||||||
std::string text;
|
|
||||||
g.value(text, 0);
|
|
||||||
texts.push_back(text);
|
|
||||||
// the same value written differently: sorted keys, canonical numbers
|
|
||||||
texts.push_back(json::parse(text).dump(1));
|
|
||||||
}
|
|
||||||
for (std::size_t i = 0; i + 2 < texts.size(); ++i)
|
|
||||||
{
|
|
||||||
for (std::size_t k = i; k < i + 3; ++k)
|
|
||||||
{
|
|
||||||
CAPTURE(texts[i])
|
|
||||||
CAPTURE(texts[k])
|
|
||||||
const json_document a = json_document::parse(texts[i]);
|
|
||||||
const json_document b = json_document::parse(texts[k]);
|
|
||||||
const json ja = json::parse(texts[i]);
|
|
||||||
const json jb = json::parse(texts[k]);
|
|
||||||
CHECK((a.root() == b.root()) == (ja == jb));
|
|
||||||
CHECK((a.root() != b.root()) == (ja != jb));
|
|
||||||
CHECK((a.root() == jb) == (ja == jb));
|
|
||||||
CHECK((jb == a.root()) == (ja == jb));
|
|
||||||
CHECK((a.root() != jb) == (ja != jb));
|
|
||||||
CHECK((jb != a.root()) == (ja != jb));
|
|
||||||
|
|
||||||
// ordered_json compares members in order
|
|
||||||
const ordered_json_document oa = ordered_json_document::parse(texts[i]);
|
|
||||||
const ordered_json_document ob = ordered_json_document::parse(texts[k]);
|
|
||||||
const ordered_json oja = ordered_json::parse(texts[i]);
|
|
||||||
const ordered_json ojb = ordered_json::parse(texts[k]);
|
|
||||||
CHECK((oa.root() == ob.root()) == (oja == ojb));
|
|
||||||
CHECK((oa.root() == ojb) == (oja == ojb));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SECTION("numbers, duplicate keys, member order")
|
|
||||||
{
|
|
||||||
const auto same = [](const char* x, const char* y)
|
|
||||||
{
|
|
||||||
const json_document dx = json_document::parse(x);
|
|
||||||
const json_document dy = json_document::parse(y);
|
|
||||||
return dx.root() == dy.root();
|
|
||||||
};
|
|
||||||
CHECK(same("1", "1.0"));
|
|
||||||
CHECK(same("[1, -1, 2.5]", "[1.0, -1.0, 25e-1]"));
|
|
||||||
CHECK(!same("1", "1.5"));
|
|
||||||
CHECK(same("18446744073709551615", "18446744073709551615"));
|
|
||||||
CHECK(same(R"({"a": 1, "a": 2})", R"({"a": 2})"));
|
|
||||||
CHECK(!same(R"({"a": 1, "a": 2})", R"({"a": 1})"));
|
|
||||||
CHECK(same(R"({"a": 1, "b": 2})", R"({"b": 2, "a": 1})"));
|
|
||||||
CHECK(!same(R"({"a": 1})", R"({"a": 1, "b": 2})"));
|
|
||||||
CHECK(!same("[1, 2]", "[2, 1]"));
|
|
||||||
CHECK(!same("\"a\"", "\"b\""));
|
|
||||||
CHECK(same("\"\\u00e9\"", "\"\xc3\xa9\""));
|
|
||||||
CHECK(!same("null", "false"));
|
|
||||||
CHECK(!same("[]", "{}"));
|
|
||||||
const ordered_json_document dup = ordered_json_document::parse(R"({"a": 1, "b": 2, "a": 3})");
|
|
||||||
const ordered_json_document last = ordered_json_document::parse(R"({"a": 3, "b": 2})");
|
|
||||||
CHECK(dup.root() == last.root());
|
|
||||||
// (== compares as parse() resolves duplicates, a lookup finds the first)
|
|
||||||
CHECK(dup.root()["a"].materialize() == 1);
|
|
||||||
CHECK(dup.root()["a"] != last.root()["a"]);
|
|
||||||
const ordered_json_document ab = ordered_json_document::parse(R"({"a": 1, "b": 2})");
|
|
||||||
const ordered_json_document ba = ordered_json_document::parse(R"({"b": 2, "a": 1})");
|
|
||||||
CHECK(ab.root() != ba.root());
|
|
||||||
|
|
||||||
// discarded values compare as basic_json's do
|
|
||||||
const json discarded(json::value_t::discarded);
|
|
||||||
CHECK((json_view() == json_view()) == (discarded == discarded)); // NOLINT(readability-container-size-empty): operator== is tested
|
|
||||||
CHECK((json_view() == discarded) == (discarded == discarded)); // NOLINT(readability-container-size-empty)
|
|
||||||
const json_document null_document = json_document::parse("null");
|
|
||||||
CHECK(!(json_view() == null_document.root())); // NOLINT(readability-container-size-empty)
|
|
||||||
CHECK(!(null_document.root() == discarded));
|
|
||||||
}
|
|
||||||
|
|
||||||
SECTION("deep nesting")
|
|
||||||
{
|
|
||||||
const std::string deep = std::string(100000, '[') + std::string(100000, ']');
|
|
||||||
const json_document a = json_document::parse(deep);
|
|
||||||
const json_document b = json_document::parse(deep);
|
|
||||||
CHECK(a.root() == b.root());
|
|
||||||
CHECK(a.root() == json::parse(deep));
|
|
||||||
const std::string other = std::string(100000, '[') + "1" + std::string(100000, ']');
|
|
||||||
const json_document c = json_document::parse(other);
|
|
||||||
CHECK(a.root() != c.root());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_CASE("json_view large objects")
|
TEST_CASE("json_view large objects")
|
||||||
{
|
{
|
||||||
// objects with 128 members or more are looked up with a hash index
|
// objects with 128 members or more are looked up with a hash index
|
||||||
|
|||||||
@@ -682,6 +682,64 @@ TEST_CASE("json_view images: ownership")
|
|||||||
#if !defined(JSON_NOEXCEPTION)
|
#if !defined(JSON_NOEXCEPTION)
|
||||||
TEST_CASE("json_view images: errors")
|
TEST_CASE("json_view images: errors")
|
||||||
{
|
{
|
||||||
|
SECTION("float tokens: a changed byte is accepted if the text is still a number with the digits the node records")
|
||||||
|
{
|
||||||
|
for (const char* const token :
|
||||||
|
{
|
||||||
|
"-12.375", "0.5", "123456.0", "-0.0", "9999999999999999999.5", "0.000000000000000000000001", "1.25e10", "5E-3"
|
||||||
|
})
|
||||||
|
{
|
||||||
|
CAPTURE(token)
|
||||||
|
const std::string text = std::string("[") + token + "]";
|
||||||
|
const json_document d = json_document::parse(text);
|
||||||
|
const std::vector<std::uint8_t> image = d.save();
|
||||||
|
const std::size_t at = text_at(image) + 1; // the first byte of the token
|
||||||
|
const std::uint16_t recorded = node_at(image, 1).extra;
|
||||||
|
CHECK(loaded_dump(image) == json::parse(text).dump());
|
||||||
|
for (std::size_t pos = 0; token[pos] != '\0'; ++pos)
|
||||||
|
{
|
||||||
|
for (unsigned int byte = 0; byte < 256; ++byte)
|
||||||
|
{
|
||||||
|
std::vector<std::uint8_t> changed = image;
|
||||||
|
changed[at + pos] = static_cast<std::uint8_t>(byte);
|
||||||
|
std::string t = token;
|
||||||
|
t[pos] = static_cast<char>(byte);
|
||||||
|
// the digits of the integer part and of the fraction of the changed token
|
||||||
|
std::size_t integer = 0;
|
||||||
|
std::size_t fraction = 0;
|
||||||
|
std::size_t k = (t[0] == '-') ? 1 : 0;
|
||||||
|
for (; k < t.size() && t[k] >= '0' && t[k] <= '9'; ++k)
|
||||||
|
{
|
||||||
|
++integer;
|
||||||
|
}
|
||||||
|
if (k < t.size() && t[k] == '.')
|
||||||
|
{
|
||||||
|
for (++k; k < t.size() && t[k] >= '0' && t[k] <= '9'; ++k)
|
||||||
|
{
|
||||||
|
++fraction;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// (as a JSON text, the token may have white space around it)
|
||||||
|
const bool float_token = json::accept(t) && t.find_first_of(std::string(" \t\r\n\0", 5)) == std::string::npos && (t.find('.') != std::string::npos || t.find_first_of("eE") != std::string::npos) // NOLINT(abseil-string-find-str-contains)
|
||||||
|
&& ((integer | (fraction << 8U)) == recorded);
|
||||||
|
CAPTURE(pos)
|
||||||
|
CAPTURE(byte)
|
||||||
|
std::string result;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
result = loaded_dump(changed);
|
||||||
|
}
|
||||||
|
catch (const json::parse_error& e)
|
||||||
|
{
|
||||||
|
CHECK(std::string(e.what()) == check_failed);
|
||||||
|
result = "rejected";
|
||||||
|
}
|
||||||
|
CHECK((result != "rejected") == float_token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SECTION("invalid UTF-8 of a decoded string: the compact dump writes it, the others throw")
|
SECTION("invalid UTF-8 of a decoded string: the compact dump writes it, the others throw")
|
||||||
{
|
{
|
||||||
const json_document d = json_document::parse(R"({"k\n":"\u00e9x"})");
|
const json_document d = json_document::parse(R"({"k\n":"\u00e9x"})");
|
||||||
|
|||||||
Reference in new issue
Block a user