Public Access
Updates from Gitea, step 1: the model (host-tested)
A streaming JSON scanner (a 33 KB list of releases costs a few hundred bytes), the release reader built on it, HTTP response heads and chunked bodies, URLs, and the decisions: which release is an update, whether to announce it, and which download URLs the device takes. Tested against the real answers of git.twis.la. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
#include <unity.h>
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "json_scan.h"
|
||||
|
||||
using namespace roro::release;
|
||||
|
||||
void setUp() {}
|
||||
void tearDown() {}
|
||||
|
||||
struct Seen {
|
||||
std::vector<std::string> paths;
|
||||
std::map<std::string, std::string> values;
|
||||
std::map<std::string, bool> strings, cut;
|
||||
};
|
||||
|
||||
static Seen scan(const std::string& json, size_t piece, size_t max = 300, bool* failed = nullptr, bool* done = nullptr) {
|
||||
Seen seen;
|
||||
JsonScanner s(
|
||||
[&](const std::string& path, const std::string& value, bool isString, bool truncated) {
|
||||
seen.paths.push_back(path);
|
||||
seen.values[path] = value;
|
||||
seen.strings[path] = isString;
|
||||
seen.cut[path] = truncated;
|
||||
},
|
||||
max);
|
||||
for (size_t at = 0; at < json.size(); at += piece) s.feed(json.data() + at, std::min(piece, json.size() - at));
|
||||
if (failed) *failed = s.failed();
|
||||
if (done) *done = s.done();
|
||||
return seen;
|
||||
}
|
||||
|
||||
void test_paths_and_kinds() {
|
||||
std::string json = R"({"tag_name":"v0.10.0","draft":false,"size":1885712,"ratio":-1.5e3,"none":null,
|
||||
"assets":[{"name":"a.ota","size":12},{"name":"b.bin","size":3,"tags":[]},{"name":"c","nested":{"deep":[true,"x"]}}],
|
||||
"empty":{},"last":"end"})";
|
||||
for (size_t piece : {size_t(1), size_t(3), size_t(7), json.size()}) {
|
||||
bool failed, done;
|
||||
Seen s = scan(json, piece, 300, &failed, &done);
|
||||
TEST_ASSERT_FALSE(failed);
|
||||
TEST_ASSERT_TRUE(done);
|
||||
TEST_ASSERT_EQUAL_STRING("v0.10.0", s.values["tag_name"].c_str());
|
||||
TEST_ASSERT_TRUE(s.strings["tag_name"]);
|
||||
TEST_ASSERT_EQUAL_STRING("false", s.values["draft"].c_str());
|
||||
TEST_ASSERT_FALSE(s.strings["draft"]);
|
||||
TEST_ASSERT_EQUAL_STRING("1885712", s.values["size"].c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("-1.5e3", s.values["ratio"].c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("null", s.values["none"].c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("a.ota", s.values["assets[0].name"].c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("3", s.values["assets[1].size"].c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("true", s.values["assets[2].nested.deep[0]"].c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("x", s.values["assets[2].nested.deep[1]"].c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("end", s.values["last"].c_str());
|
||||
TEST_ASSERT_EQUAL_size_t(0, s.values.count("assets[1].tags")); // empty containers report nothing
|
||||
TEST_ASSERT_EQUAL_size_t(0, s.values.count("empty"));
|
||||
}
|
||||
}
|
||||
|
||||
void test_top_level_array_and_scalars() {
|
||||
Seen s = scan(R"([{"a":1},{"a":2},"x"])", 4);
|
||||
TEST_ASSERT_EQUAL_STRING("2", s.values["[1].a"].c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("x", s.values["[2]"].c_str());
|
||||
bool done;
|
||||
s = scan("42", 1, 300, nullptr, &done); // a number ends only when something follows it
|
||||
TEST_ASSERT_FALSE(done);
|
||||
s = scan("42 ", 1, 300, nullptr, &done);
|
||||
TEST_ASSERT_TRUE(done);
|
||||
TEST_ASSERT_EQUAL_STRING("42", s.values[""].c_str());
|
||||
}
|
||||
|
||||
void test_escapes_and_unicode() {
|
||||
std::string json = R"({"s":"line1\nline2\t\"q\" \\ \/ caf\u00e9 \u20ac \ud83d\ude00 end"})";
|
||||
for (size_t piece : {size_t(1), size_t(5), json.size()}) {
|
||||
Seen s = scan(json, piece);
|
||||
TEST_ASSERT_EQUAL_STRING("line1\nline2\t\"q\" \\ / caf\xC3\xA9 \xE2\x82\xAC \xF0\x9F\x98\x80 end", s.values["s"].c_str());
|
||||
}
|
||||
std::string key = R"({"a\"b":1})";
|
||||
TEST_ASSERT_EQUAL_STRING("1", scan(key, 2).values["a\"b"].c_str());
|
||||
}
|
||||
|
||||
void test_long_values_are_cut_on_a_character() {
|
||||
std::string json = "{\"body\":\"" + std::string(20, 'a') + "\xC3\xA9\xC3\xA9\"}"; // 24 bytes
|
||||
Seen s = scan(json, 3, 21); // room for 21: the é would be cut
|
||||
TEST_ASSERT_EQUAL_STRING(std::string(20, 'a').c_str(), s.values["body"].c_str());
|
||||
TEST_ASSERT_TRUE(s.cut["body"]);
|
||||
s = scan(json, 3, 22);
|
||||
TEST_ASSERT_EQUAL_size_t(22, s.values["body"].size());
|
||||
s = scan(json, 3, 100);
|
||||
TEST_ASSERT_FALSE(s.cut["body"]);
|
||||
// What follows a cut value is still read.
|
||||
s = scan(R"({"a":"0123456789","b":"ok"})", 2, 4);
|
||||
TEST_ASSERT_EQUAL_STRING("0123", s.values["a"].c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("ok", s.values["b"].c_str());
|
||||
}
|
||||
|
||||
void test_what_it_can_not_follow() {
|
||||
bool failed;
|
||||
scan("{\"a\" 1}", 1, 300, &failed);
|
||||
TEST_ASSERT_TRUE(failed);
|
||||
scan("{\"a\":1,,}", 1, 300, &failed);
|
||||
TEST_ASSERT_TRUE(failed);
|
||||
scan("[1,2}", 1, 300, &failed);
|
||||
TEST_ASSERT_TRUE(failed);
|
||||
scan("<html>not json</html>", 1, 300, &failed);
|
||||
TEST_ASSERT_TRUE(failed);
|
||||
scan("{\"a\":\"\\u12G4\"}", 1, 300, &failed);
|
||||
TEST_ASSERT_TRUE(failed);
|
||||
bool done;
|
||||
scan("{\"a\":[1,2", 2, 300, &failed, &done); // cut short: not an error, not done
|
||||
TEST_ASSERT_FALSE(failed);
|
||||
TEST_ASSERT_FALSE(done);
|
||||
}
|
||||
|
||||
int main() {
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_paths_and_kinds);
|
||||
RUN_TEST(test_top_level_array_and_scalars);
|
||||
RUN_TEST(test_escapes_and_unicode);
|
||||
RUN_TEST(test_long_values_are_cut_on_a_character);
|
||||
RUN_TEST(test_what_it_can_not_follow);
|
||||
return UNITY_END();
|
||||
}
|
||||
Reference in New Issue
Block a user