/
nv-lang
/
nova
Обзор
Документация
Войти
/
nv-lang
/
nova
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
main
std/src/encoding/json_test.nv
339 строк
13 KB
Evgeniy Golovin
fix(std/encoding/json, реестр 221.1 №148): JsonObject — детерминированный порядок полей [M-json-encode-record-field-order-nondeterministic]
29 июл 2026, 19:52
29 июл 2026, 19:52
e7d8cc9
Код
Авторство
О чём код?
// Тесты encoding.json — RFC 8259 conformance + round-trip. // // Peer-файл модуля `encoding.json` (json.nv) — feedback-module-tests-beside-module: // тесты std-модулей рядом с модулем, не в spec_tests/nova_tests. D78/D29 rev-3: // это НЕ folder-модуль (encoding/ группирует несколько независимых single-file // модулей: encoding.json, encoding.base64, ...) — тестовый peer получает свой // module path `parent.filestem` (encoding.json_test), поэтому все имена типа // JsonValue/ParseJsonError импортируются и варианты квалифицируются явно. module encoding.json_test import std.encoding.json.{Json, JsonValue, JsonObject, ParseJsonError, pretty} test "parse: примитивы" { assert(Json.parse("null")!!.is_null()) assert(Json.parse("true")!!.bool() == Some(true)) assert(Json.parse("false")!!.bool() == Some(false)) assert(Json.parse("42")!!.num() == Some(42.0)) assert(Json.parse("-3.14")!!.num() == Some(-3.14)) assert(Json.parse("1e3")!!.num() == Some(1000.0)) assert(Json.parse("\"hello\"")!!.str() == Some("hello")) } test "parse: пустые collections" { ro arr = Json.parse("[]")!! assert(arr.is_array()) match arr.array() { Some(xs) => assert(xs.len() == 0) None => assert(false) } ro obj = Json.parse("{}")!! assert(obj.is_object()) } test "parse: array элементов" { ro v = Json.parse("[1, 2, 3]")!! match v.array() { Some(xs) => { assert(xs.len() == 3) assert(xs[0].num() == Some(1.0)) assert(xs[1].num() == Some(2.0)) assert(xs[2].num() == Some(3.0)) } None => assert(false) } } test "parse: object с полями" { ro v = Json.parse("{\"name\": \"alice\", \"age\": 30}")!! match v.object() { Some(m) => { assert(m.get("name") == Some(JsonValue.Str("alice"))) assert(m.get("age") == Some(JsonValue.Num(30.0))) } None => assert(false) } } test "parse: nested structures" { ro s = "{\"users\": [{\"id\": 1}, {\"id\": 2}]}" ro v = Json.parse(s)!! match v.object() { Some(root) => match root.get("users") { Some(users_v) => match users_v.array() { Some(users) => { assert(users.len() == 2) assert(users[0].object().is_some()) } None => assert(false) } None => assert(false) } None => assert(false) } } test "parse: escape sequences в string" { ro v = Json.parse("\"line1\\nline2\\ttab\\\"quote\"")!! assert(v.str() == Some("line1\nline2\ttab\"quote")) } test "parse: unicode escape" { ro v = Json.parse("\"\\u00E9\"")!! // é assert(v.str() == Some("é")) } test "parse: surrogate pair (emoji)" { ro v = Json.parse("\"\\uD83D\\uDE00\"")!! // 😀 (U+1F600) match v.str() { Some(s) => assert(s.byte_len() > 0) None => assert(false) } } test "parse: ошибка — неизвестный токен" { ro r = Json.parse("xyz") assert(match r { Err(ParseJsonError.UnexpectedChar { .. }) => true _ => false }) } // [M-json-byte-peek]: диагностика non-ASCII мусора несёт НАСТОЯЩИЙ символ, // не Latin-1-фантом байта (@peek()/@advance() честно -> Option[u8], но // BadTok(char)/UnexpectedChar{found} декодируют реальный codepoint на // границе конструирования ошибки — см. Lexer.@char_at в json.nv). test "parse: ошибка — non-ASCII мусор несёт настоящий символ" { ro r = Json.parse("é") assert(match r { Err(ParseJsonError.UnexpectedChar { found, .. }) => found == 'é' _ => false }) } test "parse: ошибка — незакрытая string" { ro r = Json.parse("\"unclosed") assert(match r { Err(ParseJsonError.UnexpectedEof { .. }) => true _ => false }) } test "parse: ошибка — trailing content" { ro r = Json.parse("42 garbage") assert(match r { Err(ParseJsonError.TrailingContent { .. }) => true _ => false }) } test "parse: ошибка — duplicate key" { ro r = Json.parse("{\"a\": 1, \"a\": 2}") assert(match r { Err(ParseJsonError.DuplicateKey { .. }) => true _ => false }) } test "parse: ошибка — bad number" { ro r = Json.parse("01") // leading zero запрещён в JSON assert(match r { Err(ParseJsonError.TrailingContent { .. }) | Err(ParseJsonError.InvalidNumber { .. }) => true _ => false }) } test "to_str: примитивы — round-trip" { ro cases = [ JsonValue.Null, JsonValue.Bool(true), JsonValue.Bool(false), JsonValue.Num(42.0), JsonValue.Num(-3.14), JsonValue.Str("hello"), ] for v in cases { ro s = v.to_str() ro parsed = Json.parse(s)!! assert(parsed == v) } } test "to_str: array round-trip" { ro v = JsonValue.Array([JsonValue.Num(1.0), JsonValue.Num(2.0), JsonValue.Str("three")]) ro s = v.to_str() ro parsed = Json.parse(s)!! assert(parsed == v) } test "to_str: nested object round-trip" { mut m = JsonObject.new() m.insert("name", JsonValue.Str("alice")) m.insert("score", JsonValue.Num(95.5)) ro v = JsonValue.Object(m) ro s = v.to_str() ro parsed = Json.parse(s)!! assert(parsed == v) } test "to_str: escape в string" { ro v = JsonValue.Str("line1\nline2\ttab\"quote") ro s = v.to_str() ro parsed = Json.parse(s)!! assert(parsed == v) } test "D325: JsonValue.try_from(s) и Json.parse(s) работают" { ro v JsonValue = JsonValue.try_from("[1, 2, 3]")!! assert(v.is_array()) ro v2 = Json.parse("[1, 2, 3]")!! assert(v == v2) } test "pretty: формат с отступами" { mut m = JsonObject.new() m.insert("name", JsonValue.Str("alice")) ro v = JsonValue.Object(m) ro p = v.to_str_pretty() // Проверяем что есть newlines (формат multi-line) assert(p.contains("\n")) // Round-trip через парсер всё ещё работает assert(Json.parse(p)!! == v) } test "D422: ${v:#} = to_str_pretty(), ${v} = to_str() (unified @display)" { mut m = JsonObject.new() m.insert("name", JsonValue.Str("alice")) ro v = JsonValue.Object(m) assert("${v:#}" == v.to_str_pretty()) assert("${v}" == v.to_str()) } // ────────────────────────────────────────────────────────────────────────── // [M-json-encode-record-field-order-nondeterministic] (реестр 221.1 №148): // порядок полей `Object` обязан совпадать с порядком ВСТАВКИ (для // record-DTO — порядком объявления полей) и быть детерминированным между // запусками. Внутрипроцессный тест НЕ ловит межпроцессную недетерминированность // (HashMap-итерация раньше расходилась только между разными process'ами — // per-process randomized siphash seed) — см. также отдельную межпроцессную // проверку в отчёте задачи (два запуска бинаря, побайтовое сравнение). // ────────────────────────────────────────────────────────────────────────── test "JsonObject: порядок вставки сохраняется при @iter()/@keys()" { mut m = JsonObject.new() m.insert("z_first", JsonValue.Num(1.0)) m.insert("a_second", JsonValue.Num(2.0)) m.insert("m_third", JsonValue.Num(3.0)) ro keys = m.keys() assert(keys.len() == 3) assert(keys[0] == "z_first") assert(keys[1] == "a_second") assert(keys[2] == "m_third") mut collected []str = [] for (k, _v) in m.iter() { collected.push(k) } assert(collected == ["z_first", "a_second", "m_third"]) } test "JsonObject: повторная вставка ключа НЕ двигает позицию" { mut m = JsonObject.new() m.insert("a", JsonValue.Num(1.0)) m.insert("b", JsonValue.Num(2.0)) m.insert("a", JsonValue.Num(99.0)) // обновление, не re-append ro keys = m.keys() assert(keys.len() == 2) assert(keys[0] == "a") assert(keys[1] == "b") assert(m.get("a") == Some(JsonValue.Num(99.0))) } // Пин конкретного ожидаемого порядка полей — record из 3 полей, порядок // ОБЪЯВЛЕНИЯ (не hash-порядок, не сортировка). test "to_str: порядок полей Object детерминирован — пин конкретного вывода" { mut m = JsonObject.new() m.insert("id", JsonValue.Str("weather-1")) m.insert("elapsed_ms", JsonValue.Num(42.0)) m.insert("t_ms", JsonValue.Num(100.0)) ro v = JsonValue.Object(m) assert(v.to_str() == "{\"id\":\"weather-1\",\"elapsed_ms\":42,\"t_ms\":100}") } // Round-trip: parse -> encode сохраняет порядок полей ИЗ ТЕКСТА (не hash-порядок). test "parse->to_str: порядок полей из текста сохраняется round-trip" { ro src = "{\"t_ms\":100,\"id\":\"weather-1\",\"elapsed_ms\":42}" ro v = Json.parse(src)!! assert(v.to_str() == src) } test "whitespace терпимость" { ro v = Json.parse(" [ 1 , 2 ] ")!! match v.array() { Some(xs) => assert(xs.len() == 2) None => assert(false) } } test "числа: разные форматы" { assert(Json.parse("0")!!.num() == Some(0.0)) assert(Json.parse("-0")!!.num() == Some(-0.0) || Json.parse("-0")!!.num() == Some(0.0)) assert(Json.parse("1.5e2")!!.num() == Some(150.0)) assert(Json.parse("2.5E-1")!!.num() == Some(0.25)) assert(Json.parse("-1.5e+3")!!.num() == Some(-1500.0)) } // ────────────────────────────────────────────────────────────────────────── // Регресс-тесты: json-санация (Баг 1 byte/codepoint lexer, Баг 2 \b\f escape) // ────────────────────────────────────────────────────────────────────────── // [M-json-lexer-byte-cursor] (Баг 1): non-ASCII в строке БЕЗ escape + число // сразу после — раньше @pos (codepoint-курсор через nth) расходился с // байтовыми срезами @read_number, портя число. test "Баг 1: non-ASCII в строке без escape + число после" { ro v = Json.parse("[\"é\", 12]")!! match v.array() { Some(xs) => { assert(xs.len() == 2) assert(xs[0].str() == Some("é")) assert(xs[1].num() == Some(12.0)) } None => assert(false) } } // Многобайтовый codepoint прямо в ключе объекта. test "Баг 1: многобайтовый codepoint в ключе объекта" { ro v = Json.parse("{\"héllo\": 1}")!! match v.object() { Some(m) => assert(m.get("héllo") == Some(JsonValue.Num(1.0))) None => assert(false) } } // [M-json-escape-bf-empty] (Баг 2): \b/\f обязаны декодироваться в // codepoint U+0008/U+000C (не в пустую строку) и сериализоваться обратно. test "Баг 2: round-trip \\b\\f" { ro v = Json.parse("\"\\b\\f\"")!! match v.str() { Some(s) => { assert(s.byte_len() == 2) ro bytes = s.bytes() assert(bytes[0] == 8) assert(bytes[1] == 12) } None => assert(false) } ro s = v.to_str() ro parsed = Json.parse(s)!! assert(parsed == v) }