From d3c0b8767f843aa40f708bddd6ccfacca407be5b Mon Sep 17 00:00:00 2001 From: harehare Date: Sat, 30 May 2026 10:56:23 +0900 Subject: [PATCH] test: add tests and CI workflow Add json5_tests.mq with 53 tests covering strings, numbers, booleans, arrays, objects, comments, and helper functions. Add GitHub Actions CI using harehare/setup-mq to run mq-test on push and pull request. --- .github/workflows/ci.yml | 23 +++ json5.mq | 15 +- json5_tests.mq | 310 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 340 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 json5_tests.mq diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..0288cc4 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,23 @@ +name: CI + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + - uses: harehare/setup-mq@25dd7cb844804420bcf785503ea4ca54e7fb729c # v1 + with: + bins: test + - name: Run tests + run: mq-test json5_tests.mq diff --git a/json5.mq b/json5.mq index 4d5a1e4..2efde38 100644 --- a/json5.mq +++ b/json5.mq @@ -137,7 +137,7 @@ def _j5_lex(s): | let has_sign = starts_with(m, "-") || starts_with(m, "+") | let hex_part = if (has_sign): slice(m, 3, len(m)) else: slice(m, 2, len(m)) | let num = if (neg): 0 - _j5_hex_to_int(hex_part) else: _j5_hex_to_int(hex_part) - | tokens += [{"t": "num", "v": num}] + | tokens += [{"t": "num", "v": to_string(num)}] | pos += len(m) | tokens end @@ -145,17 +145,17 @@ def _j5_lex(s): do let m = first(regex_match(slice(s, pos, n), "^[+-]?(?:Infinity|NaN)")) | let dm = downcase(m) - | let num = if (dm == "nan"): to_number("NaN") - elif (dm == "infinity" || dm == "+infinity"): to_number("inf") - else: to_number("-inf") - | tokens += [{"t": "num", "v": num}] + | let num_str = if (dm == "nan"): "nan" + elif (dm == "infinity" || dm == "+infinity"): "inf" + else: "-inf" + | tokens += [{"t": "num", "v": num_str}] | pos += len(m) | tokens end elif (is_regex_match(slice(s, pos, n), "^[+-]?(?:[0-9]*\\.[0-9]+|[0-9]+\\.[0-9]*|[0-9]+)(?:[eE][+-]?[0-9]+)?")): do let m = first(regex_match(slice(s, pos, n), "^[+-]?(?:[0-9]*\\.[0-9]+|[0-9]+\\.[0-9]*|[0-9]+)(?:[eE][+-]?[0-9]+)?")) - | tokens += [{"t": "num", "v": to_number(m)}] + | tokens += [{"t": "num", "v": m}] | pos += len(m) | tokens end @@ -260,7 +260,7 @@ def _j5_parse_value(tokens, pos): | if (tt == "str"): [pos + 1, tok["v"]] elif (tt == "num"): - [pos + 1, tok["v"]] + [pos + 1, to_number(tok["v"])] elif (tt == "id" && tok["v"] == "true"): [pos + 1, true] elif (tt == "id" && tok["v"] == "false"): @@ -291,4 +291,3 @@ def json5_parse(input): r[1] end end - diff --git a/json5_tests.mq b/json5_tests.mq new file mode 100644 index 0000000..7027cc7 --- /dev/null +++ b/json5_tests.mq @@ -0,0 +1,310 @@ +include "test" +| include "json5" +| + +# -- String Tests -- + +def test_parse_double_quoted_string(): + let result = json5_parse("\"hello\"") + | assert_eq(result, "hello") +end + +def test_parse_single_quoted_string(): + let result = json5_parse("'hello'") + | assert_eq(result, "hello") +end + +def test_parse_empty_string(): + let result = json5_parse("\"\"") + | assert_eq(result, "") +end + +def test_parse_string_with_escape_n(): + let result = json5_parse("\"line1\\nline2\"") + | assert_eq(result, "line1\nline2") +end + +def test_parse_string_with_escape_t(): + let result = json5_parse("\"col1\\tcol2\"") + | assert_eq(result, "col1\tcol2") +end + +def test_parse_string_with_escape_r(): + let result = json5_parse("\"a\\rb\"") + | assert_eq(result, "a\rb") +end + +def test_parse_string_with_escaped_backslash(): + let result = json5_parse("\"a\\\\b\"") + | assert_eq(result, "a\\b") +end + +def test_parse_string_with_escaped_quote(): + let result = json5_parse("\"say \\\"hi\\\"\"") + | assert_eq(result, "say \"hi\"") +end + +def test_parse_string_with_escaped_single_quote(): + let result = json5_parse("'it\\'s'") + | assert_eq(result, "it's") +end + +def test_parse_string_with_unicode_escape(): + let result = json5_parse("\"\\u0041\"") + | assert_eq(result, "A") +end + +def test_parse_string_with_hex_escape(): + let result = json5_parse("\"\\x41\"") + | assert_eq(result, "A") +end + +def test_parse_string_with_null_escape(): + let result = json5_parse("\"a\\0b\"") + | assert_eq(len(result), 3) +end + +# -- Number Tests -- + +def test_parse_integer(): + let result = json5_parse("42") + | assert_eq(result, 42) +end + +def test_parse_negative_integer(): + let result = json5_parse("-7") + | assert_eq(result, -7) +end + +def test_parse_positive_sign(): + let result = json5_parse("+3") + | assert_eq(result, 3) +end + +def test_parse_float(): + let result = json5_parse("3.14") + | assert_eq(result, 3.14) +end + +def test_parse_float_leading_dot(): + let result = json5_parse(".5") + | assert_eq(result, 0.5) +end + +def test_parse_float_trailing_dot(): + let result = json5_parse("1.") + | assert_eq(result, 1) +end + +def test_parse_exponent(): + let result = json5_parse("1e2") + | assert_eq(result, 100) +end + +def test_parse_negative_exponent(): + let result = json5_parse("1e-1") + | assert_eq(result, 0.1) +end + +def test_parse_hex_number(): + let result = json5_parse("0xFF") + | assert_eq(result, 255) +end + +def test_parse_hex_lowercase(): + let result = json5_parse("0xff") + | assert_eq(result, 255) +end + +def test_parse_hex_negative(): + let result = json5_parse("-0x10") + | assert_eq(result, -16) +end + +def test_parse_infinity(): + let result = json5_parse("Infinity") + | assert(result > 1e300) +end + +def test_parse_negative_infinity(): + let result = json5_parse("-Infinity") + | assert(result < -1e300) +end + +def test_parse_nan(): + let result = json5_parse("NaN") + | assert(result != result) +end + +# -- Boolean and Null Tests -- + +def test_parse_true(): + let result = json5_parse("true") + | assert_eq(result, true) +end + +def test_parse_false(): + let result = json5_parse("false") + | assert_eq(result, false) +end + +def test_parse_null(): + let result = json5_parse("null") + | assert_none(result) +end + +# -- Array Tests -- + +def test_parse_empty_array(): + let result = json5_parse("[]") + | assert_eq(result, []) +end + +def test_parse_simple_array(): + let result = json5_parse("[1, 2, 3]") + | assert_eq(result, [1, 2, 3]) +end + +def test_parse_mixed_array(): + let result = json5_parse("[1, \"two\", true, null]") + | assert_eq(len(result), 4) + | assert_eq(result[0], 1) + | assert_eq(result[1], "two") + | assert_eq(result[2], true) + | assert_none(result[3]) +end + +def test_parse_array_trailing_comma(): + let result = json5_parse("[1, 2, 3,]") + | assert_eq(result, [1, 2, 3]) +end + +def test_parse_nested_array(): + let result = json5_parse("[[1, 2], [3, 4]]") + | assert_eq(result[0], [1, 2]) + | assert_eq(result[1], [3, 4]) +end + +# -- Object Tests -- + +def test_parse_empty_object(): + let result = json5_parse("{}") + | assert_eq(result, dict()) +end + +def test_parse_simple_object(): + let result = json5_parse("{\"a\": 1, \"b\": 2}") + | assert_eq(result["a"], 1) + | assert_eq(result["b"], 2) +end + +def test_parse_object_unquoted_key(): + let result = json5_parse("{name: \"Alice\", age: 30}") + | assert_eq(result["name"], "Alice") + | assert_eq(result["age"], 30) +end + +def test_parse_object_single_quoted_key(): + let result = json5_parse("{'key': 'value'}") + | assert_eq(result["key"], "value") +end + +def test_parse_object_trailing_comma(): + let result = json5_parse("{\"x\": 1,}") + | assert_eq(result["x"], 1) +end + +def test_parse_nested_object(): + let result = json5_parse("{\"outer\": {\"inner\": 42}}") + | assert_eq(result["outer"]["inner"], 42) +end + +def test_parse_object_mixed_values(): + let result = json5_parse("{a: 1, b: \"hello\", c: true, d: null, e: [1,2]}") + | assert_eq(result["a"], 1) + | assert_eq(result["b"], "hello") + | assert_eq(result["c"], true) + | assert_none(result["d"]) + | assert_eq(result["e"], [1, 2]) +end + +# -- Comment Tests -- + +def test_parse_line_comment(): + let result = json5_parse("// comment\n42") + | assert_eq(result, 42) +end + +def test_parse_inline_line_comment(): + let result = json5_parse("{a: 1 // comment\n}") + | assert_eq(result["a"], 1) +end + +def test_parse_block_comment(): + let result = json5_parse("/* block comment */ 42") + | assert_eq(result, 42) +end + +def test_parse_block_comment_in_object(): + let result = json5_parse("{/* comment */a: 1}") + | assert_eq(result["a"], 1) +end + +# -- Whitespace Tests -- + +def test_parse_with_whitespace(): + let result = json5_parse(" { \"a\" : 1 } ") + | assert_eq(result["a"], 1) +end + +def test_parse_with_newlines(): + let result = json5_parse("{\n a: 1,\n b: 2\n}") + | assert_eq(result["a"], 1) + | assert_eq(result["b"], 2) +end + +# -- Helper Function Tests -- + +def test_hex_to_int(): + let result = _j5_hex_to_int("ff") + | assert_eq(result, 255) + + | let result2 = _j5_hex_to_int("10") + | assert_eq(result2, 16) + + | let result3 = _j5_hex_to_int("0") + | assert_eq(result3, 0) +end + +def test_unescape_plain(): + let result = _j5_unescape("hello") + | assert_eq(result, "hello") +end + +def test_unescape_newline(): + let result = _j5_unescape("a\\nb") + | assert_eq(result, "a\nb") +end + +def test_unescape_unicode(): + let result = _j5_unescape("\\u0041") + | assert_eq(result, "A") +end + +# -- Complex Tests -- + +def test_parse_realistic_config(): + let input = "{// app config\nname: 'myapp', version: '1.0.0', debug: false, port: 8080, tags: ['web', 'api'],}" + | let result = json5_parse(input) + | assert_eq(result["name"], "myapp") + | assert_eq(result["version"], "1.0.0") + | assert_eq(result["debug"], false) + | assert_eq(result["port"], 8080) + | assert_eq(len(result["tags"]), 2) +end + +def test_parse_deeply_nested(): + let input = "{a: {b: {c: {d: 42}}}}" + | let result = json5_parse(input) + | assert_eq(result["a"]["b"]["c"]["d"], 42) +end