mirror of
https://github.com/felt/tippecanoe.git
synced 2026-10-02 08:25:40 +02:00
Tippecanoe formatted every double it wrote through milo::dtoa_milo, a vendored Grisu2. Grisu2 is fast, but it guarantees neither the shortest digit string nor the correctly rounded one: it only guarantees that what it prints parses back to the value it came from. In practice it prints a digit more than necessary about 0.16% of the time, and picks a neighbor of the correctly rounded digits about 32% of the time. This ports Russ Cox's fpfmt (https://github.com/rsc/fpfmt) to C++ in fpfmt/ and formats through it instead. fpfmt is both shortest and correctly rounded, and it is faster: full std::string formatting Grisu2 fpfmt speedup random bit patterns 156.62 ns 66.83 ns 2.34x geo coordinates 124.07 ns 58.62 ns 2.12x short decimals 69.37 ns 49.16 ns 1.41x small integers 44.18 ns 38.06 ns 1.16x digit generation only Grisu2 fpfmt speedup random bit patterns 90.07 ns 20.81 ns 4.33x geo coordinates 80.64 ns 20.18 ns 4.00x short decimals 55.61 ns 21.90 ns 2.54x small integers 40.23 ns 22.50 ns 1.79x (Intel Xeon @ 2.80GHz, g++ 13.3 -O3. `make fpfmt-bench` reproduces this, and `./fpfmt-bench -check` reruns the correctness sweep, which is why milo/dtoa_milo.h is kept even though nothing links it any more.) The port is deliberately literal, so it can be diffed against fpfmt.go. Its Short() agrees bit for bit with the Go original's on 445,640 values covering powers of ten, small integers and reciprocals, subnormals, and random bit patterns. Over 38.5 million values, fpfmt::dtoa always round trips, is never longer than Grisu2's output, and is shorter 61,329 times. Output is otherwise formatted exactly as before, including the choice between plain and exponential notation, so 26 expected test outputs change: some numbers lose digits (-26.170044999999999 becomes -26.170045), and some have a corrected final digit (9.823748927348929e+55 becomes 9.823748927348928e+55). Every changed token was checked to parse back to the identical double; none of the values themselves moved. milo/milo.h, whose only job was to declare the C shim jsonpull calls, is replaced by fpfmt/fpfmt.h, and the shim is renamed dtoa_shortest. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014wJRAuhMninQE4wK2TUfuZ
604 lines
22 KiB
C++
604 lines
22 KiB
C++
#define CATCH_CONFIG_MAIN
|
|
#include "catch/catch.hpp"
|
|
#include "text.hpp"
|
|
#include "sort.hpp"
|
|
#include "tile-cache.hpp"
|
|
#include "mvt.hpp"
|
|
#include "projection.hpp"
|
|
#include "geometry.hpp"
|
|
#include "jsonpull/jsonpull.h"
|
|
#include "fpfmt/fpfmt.hpp"
|
|
#include <unistd.h>
|
|
#include <cmath>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <random>
|
|
#include <limits.h>
|
|
|
|
TEST_CASE("UTF-8 enforcement", "[utf8]") {
|
|
REQUIRE(check_utf8("") == std::string(""));
|
|
REQUIRE(check_utf8("hello world") == std::string(""));
|
|
REQUIRE(check_utf8("Καλημέρα κόσμε") == std::string(""));
|
|
REQUIRE(check_utf8("こんにちは 世界") == std::string(""));
|
|
REQUIRE(check_utf8("👋🌏") == std::string(""));
|
|
REQUIRE(check_utf8("Hola m\xF3n") == std::string("\"Hola m\xF3n\" is not valid UTF-8 (0xF3 0x6E)"));
|
|
}
|
|
|
|
TEST_CASE("UTF-8 truncation", "[trunc]") {
|
|
REQUIRE(truncate16("0123456789abcdefghi", 16) == std::string("0123456789abcdef"));
|
|
REQUIRE(truncate16("0123456789éîôüéîôüç", 16) == std::string("0123456789éîôüéî"));
|
|
REQUIRE(truncate16("0123456789😀😬😁😂😃😄😅😆", 16) == std::string("0123456789😀😬😁"));
|
|
REQUIRE(truncate16("0123456789😀😬😁😂😃😄😅😆", 17) == std::string("0123456789😀😬😁"));
|
|
REQUIRE(truncate16("0123456789あいうえおかきくけこさ", 16) == std::string("0123456789あいうえおか"));
|
|
|
|
REQUIRE(truncate_string("789éîôüéîôüç", 3) == std::string("789"));
|
|
REQUIRE(truncate_string("789éîôüéîôüç", 4) == std::string("789"));
|
|
REQUIRE(truncate_string("789éîôüéîôüç", 5) == std::string("789é"));
|
|
REQUIRE(truncate_string("789éîôüéîôüç", 6) == std::string("789é"));
|
|
REQUIRE(truncate_string("789éîôüéîôüç", 7) == std::string("789éî"));
|
|
REQUIRE(truncate_string("789éîôüéîôüç", 8) == std::string("789éî"));
|
|
|
|
REQUIRE(truncate_string("0123456789😀😬😁😂😃😄😅😆", 10) == std::string("0123456789"));
|
|
REQUIRE(truncate_string("0123456789😀😬😁😂😃😄😅😆", 11) == std::string("0123456789"));
|
|
REQUIRE(truncate_string("0123456789😀😬😁😂😃😄😅😆", 12) == std::string("0123456789"));
|
|
REQUIRE(truncate_string("0123456789😀😬😁😂😃😄😅😆", 13) == std::string("0123456789"));
|
|
REQUIRE(truncate_string("0123456789😀😬😁😂😃😄😅😆", 14) == std::string("0123456789😀"));
|
|
|
|
REQUIRE(truncate_string("😀", 4) == std::string("😀"));
|
|
REQUIRE(truncate_string("😀", 3) == std::string(""));
|
|
REQUIRE(truncate_string("😀", 2) == std::string(""));
|
|
REQUIRE(truncate_string("😀", 1) == std::string(""));
|
|
REQUIRE(truncate_string("😀", 0) == std::string(""));
|
|
}
|
|
|
|
int intcmp(const void *v1, const void *v2) {
|
|
return *((int *) v1) - *((int *) v2);
|
|
}
|
|
|
|
TEST_CASE("External quicksort", "fqsort") {
|
|
std::vector<FILE *> inputs;
|
|
|
|
size_t written = 0;
|
|
for (size_t i = 0; i < 5; i++) {
|
|
std::string tmpname = "/tmp/in.XXXXXXX";
|
|
int fd = mkstemp((char *) tmpname.c_str());
|
|
unlink(tmpname.c_str());
|
|
FILE *f = fdopen(fd, "w+b");
|
|
inputs.emplace_back(f);
|
|
size_t iterations = 2000 + rand() % 200;
|
|
for (size_t j = 0; j < iterations; j++) {
|
|
int n = rand();
|
|
fwrite((void *) &n, sizeof(int), 1, f);
|
|
written++;
|
|
}
|
|
rewind(f);
|
|
}
|
|
|
|
std::string tmpname = "/tmp/out.XXXXXX";
|
|
int fd = mkstemp((char *) tmpname.c_str());
|
|
unlink(tmpname.c_str());
|
|
FILE *f = fdopen(fd, "w+b");
|
|
|
|
fqsort(inputs, sizeof(int), intcmp, f, 256, "/tmp");
|
|
rewind(f);
|
|
|
|
int prev = INT_MIN;
|
|
int here;
|
|
size_t nread = 0;
|
|
while (fread((void *) &here, sizeof(int), 1, f)) {
|
|
REQUIRE(here >= prev);
|
|
prev = here;
|
|
nread++;
|
|
}
|
|
|
|
fclose(f);
|
|
REQUIRE(nread == written);
|
|
}
|
|
|
|
mvt_tile mock_get_tile(zxy tile) {
|
|
mvt_layer l;
|
|
l.name = std::to_string(tile.z) + "/" + std::to_string(tile.x) + "/" + std::to_string(tile.y);
|
|
mvt_tile t;
|
|
t.layers.push_back(l);
|
|
return t;
|
|
}
|
|
|
|
TEST_CASE("Tile-join cache", "tile cache") {
|
|
tile_cache tc;
|
|
tc.capacity = 5;
|
|
|
|
REQUIRE(tc.get(zxy(11, 327, 791), mock_get_tile).layers[0].name == "11/327/791");
|
|
REQUIRE(tc.get(zxy(11, 5, 7), mock_get_tile).layers[0].name == "11/5/7");
|
|
REQUIRE(tc.get(zxy(11, 5, 8), mock_get_tile).layers[0].name == "11/5/8");
|
|
REQUIRE(tc.get(zxy(11, 5, 9), mock_get_tile).layers[0].name == "11/5/9");
|
|
REQUIRE(tc.get(zxy(11, 5, 10), mock_get_tile).layers[0].name == "11/5/10");
|
|
REQUIRE(tc.get(zxy(11, 327, 791), mock_get_tile).layers[0].name == "11/327/791");
|
|
REQUIRE(tc.overzoom_cache.size() == 5);
|
|
REQUIRE(tc.overzoom_cache.find(zxy(11, 327, 791)) != tc.overzoom_cache.end());
|
|
REQUIRE(tc.overzoom_cache.find(zxy(11, 5, 7)) != tc.overzoom_cache.end());
|
|
|
|
// verify that additional gets evict the least-recently-used elements
|
|
|
|
REQUIRE(tc.get(zxy(11, 5, 11), mock_get_tile).layers[0].name == "11/5/11");
|
|
REQUIRE(tc.overzoom_cache.size() == 5);
|
|
REQUIRE(tc.overzoom_cache.find(zxy(11, 5, 7)) == tc.overzoom_cache.end());
|
|
|
|
REQUIRE(tc.get(zxy(11, 5, 12), mock_get_tile).layers[0].name == "11/5/12");
|
|
REQUIRE(tc.overzoom_cache.size() == 5);
|
|
REQUIRE(tc.overzoom_cache.find(zxy(11, 5, 8)) == tc.overzoom_cache.end());
|
|
}
|
|
|
|
TEST_CASE("Bit reversal", "bit reversal") {
|
|
REQUIRE(bit_reverse(1) == 0x8000000000000000);
|
|
REQUIRE(bit_reverse(0x1234567812489BCF) == 0xF3D912481E6A2C48);
|
|
REQUIRE(bit_reverse(0xF3D912481E6A2C48) == 0x1234567812489BCF);
|
|
}
|
|
|
|
TEST_CASE("line_is_too_small") {
|
|
drawvec dv;
|
|
dv.emplace_back(VT_MOVETO, 4243099709, 2683872952);
|
|
dv.emplace_back(VT_LINETO, 4243102487, 2683873977);
|
|
dv.emplace_back(VT_MOVETO, -51867587, 2683872952);
|
|
dv.emplace_back(VT_LINETO, -51864809, 2683873977);
|
|
REQUIRE(line_is_too_small(dv, 0, 10));
|
|
}
|
|
|
|
// A high surrogate followed by a non-surrogate used to be combined into one
|
|
// code point, because the range check tested the outer-loop byte instead of
|
|
// the parsed code unit. The stale surrogate should come out as standalone
|
|
// CESU-8, then U+E000 encoded normally.
|
|
TEST_CASE("jsonpull surrogate-pair regression", "[jsonpull][surrogate]") {
|
|
json_pull_ptr jp = json_begin_string("\"\\uD83D\\uE000\"");
|
|
json_object_ptr o = json_read_tree(jp);
|
|
|
|
REQUIRE(jp->error == nullptr);
|
|
REQUIRE(o != nullptr);
|
|
REQUIRE(o->type == JSON_STRING);
|
|
|
|
const std::string expected = "\xED\xA0\xBD\xEE\x80\x80";
|
|
REQUIRE(o->string() == expected);
|
|
|
|
// Sanity check: the buggy output (a single 4-byte UTF-8 sequence for
|
|
// U+1F400) must not be what we got.
|
|
const std::string buggy = "\xF0\x9F\x90\x80";
|
|
REQUIRE(o->string() != buggy);
|
|
}
|
|
|
|
// What geojson-loop does: free each feature once serialized, so they do not
|
|
// accumulate while the rest of the document is parsed.
|
|
//
|
|
// This shape cannot catch the array-splicing bug, at any size: json_read hands
|
|
// back each container as it completes, so the node freed here is always the
|
|
// last-added element of its parent, which the old memmove got right by moving
|
|
// zero bytes. "json_free prunes a non-final element" below covers that.
|
|
TEST_CASE("json_free prunes a subtree from its parent", "[jsonpull][memory]") {
|
|
json_pull_ptr jp = json_begin_string("[[1, 2], [3, 4], [5, 6]]");
|
|
|
|
json_object *outer = nullptr;
|
|
int arrays_seen = 0;
|
|
|
|
json_object *j;
|
|
while ((j = json_read(jp)) != nullptr) {
|
|
if (j->type != JSON_ARRAY) {
|
|
continue;
|
|
}
|
|
arrays_seen++;
|
|
if (arrays_seen == 2) {
|
|
// This is [3, 4]; verify, then ask the parser to drop it.
|
|
REQUIRE(j->array().size() == 2);
|
|
REQUIRE(j->array()[0]->number() == 3);
|
|
REQUIRE(j->array()[1]->number() == 4);
|
|
json_free(j);
|
|
} else if (j->parent == nullptr) {
|
|
// The completed outer array; the parser still owns it
|
|
// via jp->root, so the borrowed pointer stays valid.
|
|
outer = j;
|
|
break;
|
|
}
|
|
}
|
|
|
|
REQUIRE(outer != nullptr);
|
|
REQUIRE(outer->type == JSON_ARRAY);
|
|
REQUIRE(outer->array().size() == 2);
|
|
|
|
// First surviving element: [1, 2].
|
|
REQUIRE(outer->array()[0]->type == JSON_ARRAY);
|
|
REQUIRE(outer->array()[0]->array().size() == 2);
|
|
REQUIRE(outer->array()[0]->array()[0]->number() == 1);
|
|
REQUIRE(outer->array()[0]->array()[1]->number() == 2);
|
|
|
|
// Second surviving element (previously third): [5, 6].
|
|
REQUIRE(outer->array()[1]->type == JSON_ARRAY);
|
|
REQUIRE(outer->array()[1]->array().size() == 2);
|
|
REQUIRE(outer->array()[1]->array()[0]->number() == 5);
|
|
REQUIRE(outer->array()[1]->array()[1]->number() == 6);
|
|
}
|
|
|
|
// A top-level value has no parent, so json_free has to drop the parser's
|
|
// reference instead. jp->root being empty afterwards is also proof the subtree
|
|
// was destroyed, since jp->root was its only owner.
|
|
TEST_CASE("json_free releases a top-level value held by the parser", "[jsonpull][memory]") {
|
|
json_pull_ptr jp = json_begin_string(R"({"a": 1, "b": [2, 3]})");
|
|
|
|
// json_read streams atoms first (1, 2, 3, [2,3], ...); the top-level
|
|
// hash is returned by the final `}` token.
|
|
json_object *top = nullptr;
|
|
json_object *j;
|
|
while ((j = json_read(jp)) != nullptr) {
|
|
if (j->parent == nullptr) {
|
|
top = j;
|
|
break;
|
|
}
|
|
}
|
|
|
|
REQUIRE(top != nullptr);
|
|
REQUIRE(top->type == JSON_HASH);
|
|
REQUIRE(jp->root.get() == top);
|
|
|
|
json_free(top);
|
|
// top is dangling now; do not dereference.
|
|
|
|
REQUIRE(jp->root == nullptr);
|
|
}
|
|
|
|
// Nothing in tippecanoe calls json_disconnect today -- the filter loaders get
|
|
// the same guarantee from json_read_tree -- so cover it here rather than leave
|
|
// a documented ownership primitive untested.
|
|
TEST_CASE("json_disconnect hands a subtree to the caller", "[jsonpull][ownership]") {
|
|
json_object_ptr taken;
|
|
json_object *outer = nullptr;
|
|
|
|
json_pull_ptr jp = json_begin_string("[[1, 2], [3, 4]]");
|
|
|
|
int arrays_seen = 0;
|
|
json_object *j;
|
|
while ((j = json_read(jp)) != nullptr) {
|
|
if (j->type != JSON_ARRAY) {
|
|
continue;
|
|
}
|
|
arrays_seen++;
|
|
if (arrays_seen == 2) {
|
|
// This is [3, 4]; take it away from the enclosing array.
|
|
taken = json_disconnect(j);
|
|
REQUIRE(taken != nullptr);
|
|
REQUIRE(taken.get() == j);
|
|
} else if (j->parent == nullptr) {
|
|
outer = j;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// The outer array is left holding only the element we didn't take,
|
|
// and the parser is still the owner of it.
|
|
REQUIRE(outer != nullptr);
|
|
REQUIRE(jp->root.get() == outer);
|
|
REQUIRE(outer->array().size() == 1);
|
|
REQUIRE(outer->array()[0]->array().size() == 2);
|
|
REQUIRE(outer->array()[0]->array()[0]->number() == 1);
|
|
REQUIRE(outer->array()[0]->array()[1]->number() == 2);
|
|
|
|
// The detached subtree holds no pointers into the parser...
|
|
REQUIRE(taken->parser == nullptr);
|
|
REQUIRE(taken->array()[0]->parser == nullptr);
|
|
REQUIRE(taken->array()[1]->parser == nullptr);
|
|
|
|
// ...and its root no longer points out at the array it was spliced
|
|
// from, but the parent links *within* it are left intact so the tree
|
|
// stays navigable upwards.
|
|
REQUIRE(taken->parent == nullptr);
|
|
REQUIRE(taken->array()[0]->parent == taken.get());
|
|
REQUIRE(taken->array()[1]->parent == taken.get());
|
|
|
|
// So the subtree stays valid once the parser, and the tree the parser
|
|
// still owns, are destroyed.
|
|
jp.reset();
|
|
// outer is dangling now; do not dereference.
|
|
|
|
REQUIRE(taken->type == JSON_ARRAY);
|
|
REQUIRE(taken->array().size() == 2);
|
|
REQUIRE(taken->array()[0]->number() == 3);
|
|
REQUIRE(taken->array()[1]->number() == 4);
|
|
}
|
|
|
|
// json_free finds a node's owner through o->parent, so clearing the interior
|
|
// parent links on detach would make this a silent no-op.
|
|
TEST_CASE("json_free prunes an interior node of a detached tree", "[jsonpull][ownership]") {
|
|
json_pull_ptr jp = json_begin_string("[[1, 2], [3, 4], [5, 6]]");
|
|
json_object_ptr tree = json_read_tree(jp);
|
|
|
|
REQUIRE(tree != nullptr);
|
|
REQUIRE(tree->type == JSON_ARRAY);
|
|
REQUIRE(tree->array().size() == 3);
|
|
|
|
// Destroy the parser first, so this is unambiguously operating on a
|
|
// tree that no longer has one.
|
|
jp.reset();
|
|
REQUIRE(tree->parser == nullptr);
|
|
|
|
json_object *drop = tree->array()[1].get();
|
|
REQUIRE(drop->array()[0]->number() == 3);
|
|
REQUIRE(drop->parent == tree.get());
|
|
|
|
json_free(drop);
|
|
// drop is dangling now; do not dereference.
|
|
|
|
// Had the parent links been cleared on detach, json_free would not
|
|
// have found an owner to splice the node out of, and the array would
|
|
// still have three elements.
|
|
REQUIRE(tree->array().size() == 2);
|
|
REQUIRE(tree->array()[0]->array()[0]->number() == 1);
|
|
REQUIRE(tree->array()[1]->array()[0]->number() == 5);
|
|
}
|
|
|
|
// Not a removal: the key stays with a JSON_NULL stand-in so the surrounding
|
|
// pairs keep their alignment, and the entry goes only when both halves do.
|
|
TEST_CASE("json_free of a hash value leaves a null placeholder", "[jsonpull][ownership]") {
|
|
json_pull_ptr jp = json_begin_string(R"({"keep": 1, "drop": [2, 3]})");
|
|
json_object_ptr tree = json_read_tree(jp);
|
|
|
|
REQUIRE(tree != nullptr);
|
|
REQUIRE(tree->entries().size() == 2);
|
|
|
|
json_object *drop = json_hash_get(tree, "drop");
|
|
REQUIRE(drop != nullptr);
|
|
REQUIRE(drop->type == JSON_ARRAY);
|
|
|
|
json_free(drop);
|
|
// drop is dangling now; do not dereference.
|
|
|
|
// The key survives, now paired with a null rather than the array.
|
|
REQUIRE(tree->entries().size() == 2);
|
|
json_object *after = json_hash_get(tree, "drop");
|
|
REQUIRE(after != nullptr);
|
|
REQUIRE(after->type == JSON_NULL);
|
|
|
|
// The neighbouring pair is untouched.
|
|
REQUIRE(json_hash_get(tree, "keep") != nullptr);
|
|
REQUIRE(json_hash_get(tree, "keep")->number() == 1);
|
|
}
|
|
|
|
// The array-splicing fix. The old code passed an element count to memmove
|
|
// where a byte count was wanted, so pruning element 0 of eight left
|
|
// arr[0] == arr[1] -- one node owned twice -- and dropped the last element.
|
|
// Asserting each survivor's identity is what discriminates; checking only the
|
|
// size would not.
|
|
TEST_CASE("json_free prunes a non-final element", "[jsonpull][ownership]") {
|
|
json_pull_ptr jp = json_begin_string("[[1], [2], [3], [4], [5], [6], [7], [8]]");
|
|
json_object_ptr tree = json_read_tree(jp);
|
|
|
|
REQUIRE(tree != nullptr);
|
|
REQUIRE(tree->type == JSON_ARRAY);
|
|
REQUIRE(tree->array().size() == 8);
|
|
|
|
json_free(tree->array()[0].get());
|
|
|
|
// Every survivor keeps its identity, in order, and nothing is aliased.
|
|
REQUIRE(tree->array().size() == 7);
|
|
for (size_t i = 0; i < tree->array().size(); i++) {
|
|
json_object *e = tree->array()[i].get();
|
|
REQUIRE(e->type == JSON_ARRAY);
|
|
REQUIRE(e->array().size() == 1);
|
|
REQUIRE(e->array()[0]->number() == (double) (i + 2));
|
|
|
|
if (i + 1 < tree->array().size()) {
|
|
REQUIRE(e != tree->array()[i + 1].get());
|
|
}
|
|
}
|
|
}
|
|
|
|
// The mirror of the value case above.
|
|
TEST_CASE("json_free of a hash key, then of both halves", "[jsonpull][ownership]") {
|
|
json_pull_ptr jp = json_begin_string(R"({"a": 1, "b": 2, "c": 3})");
|
|
json_object_ptr tree = json_read_tree(jp);
|
|
|
|
REQUIRE(tree != nullptr);
|
|
REQUIRE(tree->entries().size() == 3);
|
|
|
|
// Free the key of the middle pair. The entry stays, with a null key,
|
|
// and its value is still reachable positionally.
|
|
json_free(tree->entries()[1].key.get());
|
|
|
|
REQUIRE(tree->entries().size() == 3);
|
|
REQUIRE(tree->entries()[1].key->type == JSON_NULL);
|
|
REQUIRE(tree->entries()[1].value->number() == 2);
|
|
|
|
// The neighbours are untouched, and the now-keyless pair is no longer
|
|
// findable by name.
|
|
REQUIRE(json_hash_get(tree, "b") == nullptr);
|
|
REQUIRE(json_hash_get(tree, "a")->number() == 1);
|
|
REQUIRE(json_hash_get(tree, "c")->number() == 3);
|
|
|
|
// Freeing the other half too retires the whole entry.
|
|
json_free(tree->entries()[1].value.get());
|
|
|
|
REQUIRE(tree->entries().size() == 2);
|
|
REQUIRE(json_hash_get(tree, "a")->number() == 1);
|
|
REQUIRE(json_hash_get(tree, "c")->number() == 3);
|
|
}
|
|
|
|
// What the filter loaders and -L / -E do. Each detached tree has to survive
|
|
// the next json_read_tree call and the parser's destruction.
|
|
TEST_CASE("repeated json_read_tree on a line-delimited stream", "[jsonpull][ownership]") {
|
|
json_pull_ptr jp = json_begin_string("{\"n\": 1}\n{\"n\": 2}\n{\"n\": 3}\n");
|
|
|
|
std::vector<json_object_ptr> trees;
|
|
for (int i = 0; i < 3; i++) {
|
|
json_object_ptr t = json_read_tree(jp);
|
|
REQUIRE(t != nullptr);
|
|
REQUIRE(t->type == JSON_HASH);
|
|
// Reading the next tree must not disturb the ones already taken.
|
|
REQUIRE(json_hash_get(t, "n")->number() == i + 1);
|
|
trees.push_back(std::move(t));
|
|
}
|
|
|
|
REQUIRE(json_read_tree(jp) == nullptr);
|
|
|
|
// All three outlive the parser they came from.
|
|
jp.reset();
|
|
for (int i = 0; i < 3; i++) {
|
|
REQUIRE(trees[i]->parser == nullptr);
|
|
REQUIRE(json_hash_get(trees[i], "n")->number() == i + 1);
|
|
}
|
|
}
|
|
|
|
// What json_context() prints on the error paths: a hash whose last key has no
|
|
// value yet renders that slot as "...".
|
|
TEST_CASE("json_stringify of a partially-parsed tree", "[jsonpull][stringify]") {
|
|
json_pull_ptr jp = json_begin_string("{\"a\": [1, 2], \"b\":");
|
|
|
|
// Read until the parser runs out of input mid-hash.
|
|
while (json_read(jp) != nullptr) {
|
|
;
|
|
}
|
|
REQUIRE(jp->error != nullptr);
|
|
REQUIRE(jp->root != nullptr);
|
|
|
|
std::string s = json_stringify(jp->root.get());
|
|
REQUIRE(s == "{\"a\":[1,2],\"b\":...}");
|
|
}
|
|
|
|
// Values are std::string now, so an embedded NUL is legal and stringify has to
|
|
// walk past it rather than stop the way a c_str() loop would.
|
|
TEST_CASE("json_stringify keeps text after an embedded NUL", "[jsonpull][stringify]") {
|
|
json_pull_ptr jp = json_begin_string("\"a\\u0000b\"");
|
|
json_object_ptr o = json_read_tree(jp);
|
|
|
|
REQUIRE(o != nullptr);
|
|
REQUIRE(o->type == JSON_STRING);
|
|
REQUIRE(o->string().size() == 3);
|
|
|
|
std::string s = json_stringify(o.get());
|
|
REQUIRE(s == "\"a\\u0000b\"");
|
|
}
|
|
|
|
// U+FFFF used to fall through the `< 0xFFFF` test into the four-byte branch
|
|
// and come out as the overlong F0 8F BF BF, which check_utf8 does not catch.
|
|
TEST_CASE("jsonpull encodes U+FFFF as three bytes", "[jsonpull][utf8]") {
|
|
json_pull_ptr jp = json_begin_string("\"a\\uFFFFb\"");
|
|
json_object_ptr o = json_read_tree(jp);
|
|
|
|
REQUIRE(jp->error == nullptr);
|
|
REQUIRE(o != nullptr);
|
|
REQUIRE(o->type == JSON_STRING);
|
|
|
|
REQUIRE(o->string() ==
|
|
"a\xEF\xBF\xBF"
|
|
"b");
|
|
REQUIRE(o->string() !=
|
|
"a\xF0\x8F\xBF\xBF"
|
|
"b");
|
|
|
|
// The boundary below it, and a genuine supplementary code point built
|
|
// from a surrogate pair, both keep their existing encodings.
|
|
json_pull_ptr jp2 = json_begin_string("\"\\uFFFE\"");
|
|
json_object_ptr o2 = json_read_tree(jp2);
|
|
REQUIRE(o2 != nullptr);
|
|
REQUIRE(o2->string() == "\xEF\xBF\xBE");
|
|
|
|
json_pull_ptr jp3 = json_begin_string("\"\\uD83D\\uDC00\"");
|
|
json_object_ptr o3 = json_read_tree(jp3);
|
|
REQUIRE(o3 != nullptr);
|
|
REQUIRE(o3->string() == "\xF0\x9F\x90\x80");
|
|
}
|
|
|
|
TEST_CASE("fpfmt formats the special cases", "[fpfmt]") {
|
|
REQUIRE(fpfmt::dtoa(0.0) == std::string("0"));
|
|
REQUIRE(fpfmt::dtoa(-0.0) == std::string("0"));
|
|
REQUIRE(fpfmt::dtoa(INFINITY) == std::string("inf"));
|
|
REQUIRE(fpfmt::dtoa(-INFINITY) == std::string("-inf"));
|
|
REQUIRE(fpfmt::dtoa(NAN) == std::string("nan"));
|
|
}
|
|
|
|
TEST_CASE("fpfmt chooses plain or exponential notation", "[fpfmt]") {
|
|
REQUIRE(fpfmt::dtoa(1) == std::string("1"));
|
|
REQUIRE(fpfmt::dtoa(-1) == std::string("-1"));
|
|
REQUIRE(fpfmt::dtoa(0.1) == std::string("0.1"));
|
|
REQUIRE(fpfmt::dtoa(12.34) == std::string("12.34"));
|
|
REQUIRE(fpfmt::dtoa(-12.34) == std::string("-12.34"));
|
|
REQUIRE(fpfmt::dtoa(123456789) == std::string("123456789"));
|
|
|
|
// Plain notation runs out to 21 integer digits, and down to 1e-6.
|
|
REQUIRE(fpfmt::dtoa(1e-6) == std::string("0.000001"));
|
|
REQUIRE(fpfmt::dtoa(1e-7) == std::string("1e-7")); // no zero padding, as milo did
|
|
REQUIRE(fpfmt::dtoa(1e20) == std::string("100000000000000000000"));
|
|
REQUIRE(fpfmt::dtoa(1e21) == std::string("1e+21"));
|
|
REQUIRE(fpfmt::dtoa(1.5e300) == std::string("1.5e+300"));
|
|
REQUIRE(fpfmt::dtoa(-1.5e-300) == std::string("-1.5e-300"));
|
|
|
|
// Subnormals, and the extremes of the range.
|
|
REQUIRE(fpfmt::dtoa(5e-324) == std::string("5e-324"));
|
|
REQUIRE(fpfmt::dtoa(2.2250738585072014e-308) == std::string("2.2250738585072014e-308"));
|
|
REQUIRE(fpfmt::dtoa(1.7976931348623157e308) == std::string("1.7976931348623157e+308"));
|
|
}
|
|
|
|
TEST_CASE("fpfmt uses the shortest digit string", "[fpfmt]") {
|
|
// Cases where Grisu2 emitted a digit or more than it had to.
|
|
REQUIRE(fpfmt::dtoa(-26.170045) == std::string("-26.170045"));
|
|
REQUIRE(fpfmt::dtoa(77.272945) == std::string("77.272945"));
|
|
|
|
// Cases where Grisu2 picked a neighbor of the correctly rounded digits.
|
|
REQUIRE(fpfmt::dtoa(1.0 / 6.0) == std::string("0.16666666666666666"));
|
|
REQUIRE(fpfmt::dtoa(18446744073709551616.0) == std::string("18446744073709552000"));
|
|
}
|
|
|
|
TEST_CASE("fpfmt round trips", "[fpfmt]") {
|
|
auto round_trips = [](double d) {
|
|
std::string s = fpfmt::dtoa(d);
|
|
return strtod(s.c_str(), NULL) == d;
|
|
};
|
|
|
|
for (int i = -300; i <= 300; i++) {
|
|
double d = pow(10.0, i);
|
|
REQUIRE(round_trips(d));
|
|
REQUIRE(round_trips(-d));
|
|
}
|
|
for (int i = 1; i < 10000; i++) {
|
|
REQUIRE(round_trips((double) i));
|
|
REQUIRE(round_trips(1.0 / i));
|
|
REQUIRE(round_trips(-1.0 / i));
|
|
}
|
|
|
|
std::mt19937_64 rng(20250101);
|
|
for (int i = 0; i < 200000; i++) {
|
|
uint64_t bits = rng();
|
|
double d;
|
|
memcpy(&d, &bits, sizeof(d));
|
|
if (std::isfinite(d) && d != 0) {
|
|
REQUIRE(round_trips(d));
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Polygon cleaning drops a hole that no ring can parent", "[wagyu]") {
|
|
// Two mutually reversed self-intersecting rings whose union leaves a hole
|
|
// that wagyu's topology correction cannot assign to any surviving parent
|
|
// ring (found by fuzzing; same failure as mapbox/tippecanoe#761). Without
|
|
// the fix in mapbox/geometry/wagyu/topology_correction.hpp, this exits
|
|
// through the "Could not properly place hole to a parent." handler in
|
|
// clean_or_clip_poly instead of returning.
|
|
static const std::vector<std::vector<std::pair<long long, long long>>> rings = {
|
|
{{0, 5}, {5, 4}, {5, 1}, {4, 4}, {4, 2}, {7, 1}, {0, 5}},
|
|
{{0, 5}, {7, 1}, {4, 2}, {4, 4}, {5, 1}, {5, 4}, {0, 0}, {0, 5}},
|
|
};
|
|
|
|
drawvec geom;
|
|
for (auto const &ring : rings) {
|
|
for (size_t i = 0; i < ring.size(); i++) {
|
|
geom.push_back(draw(i == 0 ? VT_MOVETO : VT_LINETO, ring[i].first, ring[i].second));
|
|
}
|
|
}
|
|
|
|
drawvec out = clean_or_clip_poly(geom, 0, 0, false, false);
|
|
|
|
// The regression signal is getting here at all: without the fix,
|
|
// clean_or_clip_poly exits the process from its wagyu error handler.
|
|
SUCCEED("clean_or_clip_poly returned");
|
|
|
|
// Anything that survives must be sanely wound: first ring positive.
|
|
if (out.size() > 0) {
|
|
size_t j = 1;
|
|
while (j < out.size() && out[j].op == VT_LINETO) j++;
|
|
REQUIRE(get_area(out, 0, j) > 0);
|
|
}
|
|
}
|