;; This file is part of Beastie ;; SPDX-FileCopyrightText: 2025 Norman Gray ;; SPDX-License-Identifier: BSD-2-Clause (module "s7unit.scm" 'json) (print-warning 'push #f) (test-suite "JSON parsing" ;; The tests here confirm that valid JSON will parse correctly; ;; we don't (yet) try terribly hard to deal with pathologically invalid input. ;; ;; Indeed, we (now) reject with anything that isn't ;; strictly valid according to RFC 8259. ;; See the test-suite at . (let ((j (parse-json-string #"""{ "one": 1, "two": [2, "three", true, false, null], "three": {}, "four": []}"""))) (assert-equal (j 'one) 1.0) (assert-equal (j 'two) '(2.0 #"three" #t #f ())) (let ((j3 (j 'three))) (assert-true (hash-table? j3)) (assert-equal (hash-table-entries j3) 0)) (assert-equal (j 'four) '())) ;; exercise the number possibilities (assert-equal (parse-json-string "[1, 2.5, -3e2, 4.096E+3, 125e-3]") '(1.0 2.5 -300.0 4096.0 0.125)) ;; ...and strings (assert-equal (parse-json-string "\"\"") #"") ;; unterminated string (invalid JSON – fail) (assert-equal (parse-json-string "\"foo") #f) ;; ...boolean and case details (assert-equal (parse-json-string "true") #t) (assert-equal (parse-json-string "false" :on-error (λ (msg) 'no)) #f) ;; keywords are case sensitive: uppercase forbidden (assert-equal (parse-json-string "TRUE" :on-error (λ (msg) 'no)) 'no) (assert-equal (parse-json-string "FALSE" :on-error (λ (msg) 'no)) 'no) (assert-equal (parse-json-string "NULL" :on-error (λ (msg) 'no)) 'no) ;; The following is hard to write the test-case for! ;;(assert-equal (parse-json-string "\"a\\\"b\\\\c\\/d\\be\\ff\\ng\\rh\\t\\u0041i\\u2262j\"") "foo") (assert-equal (parse-json-string "\"a\\\"b\\\"\\\\c\\/d\\t\\u0041i\\u2262j\"") #"a\"b\"\\c/d Ai≢j") (assert-equal (parse-json-string "\"\\u00410\"") #"A0") ;\u should be maximum 4 characters ;;(assert-equal (parse-json-string "\"\\u41\"") #"A") ;short \u escape (assert-equal (parse-json-string "\"\\u41\"") #f) ;currently an error (assert-equal (parse-json-string "\"a\\xb\"") #f) ;bad escape, \x (assert-equal (parse-json-string "\"\\U00410\"") #f) ;\U is not a synonym for \u ;; the example of non-BMP characters, in RFC 8259, is the G-clef, U+1d11e ;; (and confirm mixed case is OK for the escape) (assert-equal (parse-json-string "\"a\\ud834\\uDD1Eb\"") #"a𝄞b") ;; Note that we _can't_ play edge-case games where we put a \u escape ;; for a high-surrogate before an actual low-surrogate, because the ;; input string is specified to be in UTF-8, where no surrogates of ;; any type can appear. ;; surrogates wrong way round (assert-equal (parse-json-string "\"a\\udd1e\\ud834b\"") #f) ;; high surrogate not followed by low (assert-equal (parse-json-string "\"a\\ud834bc\"") #f) ;; it's OK to have Unicode in strings (assert-equal (parse-json-string "\"aéb\"") #"aéb") ;; a JSON string must contain one and only one value (assert-equal (parse-json-string "") #f) (assert-equal (parse-json-string "123 \"foo\"") #f) ;; alternative error behaviour (assert-equal (parse-json-string "\"foo" :on-error (λ (msg) 'boo)) 'boo) ;; the following tests a slightly different code path (assert-equal (parse-json-string "" :on-error (λ (msg) 'boo)) 'boo)) (test-suite "JSON edge cases" ;; these tests are fairly unexpected cases, several of which are drawn from ;; https://github.com/nst/JSONTestSuite, which is referenced by ;; http://seriot.ch/parsing_json.php ;; a zero-length string as key is legal JSON, but we special-case this (assert-equal (parse-json-string "{\"\": 0}") (hash-table '_ 0.0)) ;; characters that must be escaped: RFC, Sect.7: ;; ;; All Unicode characters may be placed within the ;; quotation marks, except for the characters that MUST be escaped: ;; quotation mark, reverse solidus, and the control characters (U+0000 ;; through U+001F). (assert-false (parse-json-string "[\"new\nline\"]")) ;control characters (assert-false (parse-json-string "[\"literal tab\"]")) (assert-false (parse-json-string "\"\\x\"")) ;backslash (when not acting as escape) ) (test-suite "JSON writing" ;; we test JSON writing by whether it can be parsed by parse-json-string (let ((json (with-output-to-string (λ () ;; the alist and hashes here are single-entry, ;; so they sort trivially (for ease of testing) (json-write! (list 1.0 "a\\b" 'c #t #f '() '((alist-1 . v1)) #(1 2) (hash-table 'a (hash-table 'b #t)))))))) ;(eprintf "json=~s~%" json) (assert-equal (parse-json-string json) `(1.0 #"a\\b" #"c" #t #f () ,(hash-table 'alist-1 #"v1") (1.0 2.0) ,(hash-table 'a (hash-table 'b #t))))))