mirror of
https://github.com/felt/tippecanoe.git
synced 2026-10-02 08:25:40 +02:00
json_disconnect() is documented in jsonpull.h as the supported way to splice a subtree out of the parser's tree and take ownership of it, but nothing calls it: read_filter() and parse_filter() used to, and now get the same guarantee from json_read_tree() clearing back-pointers on the way out. Cover the behavior rather than leave the primitive dead and untested. The test pins that the subtree is removed from its parent, that the parser keeps the rest of the tree, and that the detached subtree stays readable after the json_pull is destroyed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017KNxyHKasyWrWcvre2yK4r
338 lines
13 KiB
C++
338 lines
13 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 <unistd.h>
|
|
#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));
|
|
}
|
|
|
|
// Regression test for the surrogate-decoding bug that compared the leftover
|
|
// outer-loop byte `c` against `0xdfff` instead of the parsed code unit `ch`.
|
|
// For a string like "\uD83D\uE000" (a valid high surrogate followed by a
|
|
// non-surrogate BMP code point) the buggy version would mis-classify
|
|
// U+E000 as a low surrogate and combine the two units into the four-byte
|
|
// UTF-8 sequence F0 9F 90 80 (U+1F400). The fixed version flushes the
|
|
// stale high surrogate as standalone CESU-8 (ED A0 BD) and then encodes
|
|
// U+E000 normally as EE 80 80.
|
|
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);
|
|
}
|
|
|
|
// geojson-loop.cpp calls json_free(j) after jfa->add_feature has
|
|
// serialized the feature, intending to drop the JSON subtree from the
|
|
// in-progress parse tree so that already-serialized features don't sit
|
|
// in memory while subsequent features are parsed. That intent was
|
|
// never tested; this test pins it down. The pre-fix behavior of
|
|
// json_free was a bare unique_ptr/shared_ptr reset that only dropped
|
|
// the caller's local reference; the parent container kept the subtree
|
|
// alive, so memory grew until the top-level parse completed.
|
|
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);
|
|
}
|
|
|
|
// The companion case to the pruning test above: in a line-delimited
|
|
// stream, each feature returned by json_read is a top-level value
|
|
// with no parent, but the parser still owns it via jp->root.
|
|
// json_free must drop that parser reference too, otherwise the
|
|
// just-serialized feature would sit in memory until the next feature
|
|
// started parsing. Under the unique_ptr ownership model, the only
|
|
// owner is jp->root, so verifying that jp->root is empty after the
|
|
// json_free call is also a guarantee that the subtree itself has
|
|
// been destroyed.
|
|
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);
|
|
}
|
|
|
|
// json_disconnect() is the documented way to splice a subtree out of the
|
|
// parser's tree and take ownership of it so that it can outlive the
|
|
// json_pull it came from. Nothing in tippecanoe calls it today -- the
|
|
// filter loaders get the same guarantee from json_read_tree, which clears
|
|
// back-pointers on the way out -- 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 back-pointers into the parser...
|
|
REQUIRE(taken->parent == nullptr);
|
|
REQUIRE(taken->parser == nullptr);
|
|
REQUIRE(taken->array()[0]->parser == nullptr);
|
|
REQUIRE(taken->array()[1]->parser == nullptr);
|
|
|
|
// ...so it 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);
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|