vlib/json2/json5_error_test.v
Rama master · réplica de lectura del repositorio original.
module json2
import encoding.utf8
struct Json5ErrorFields {
name string
count int
}
fn assert_json5_decode_error[T](input string, line int, character int, highlighted string) {
decode[T](input, json5: true, strict: true) or {
if err is JsonDecodeError {
assert err.line == line
assert err.character == character
assert err.context.contains('\\e[31m${highlighted}\\e[0m'), err.context
} else {
assert false, err.msg()
}
return
}
assert false, 'expected a typed JSON5 decoding error'
}
fn assert_json5_decode_error_context[T](input string, line int, character int, context string) {
decode[T](input, json5: true, strict: true) or {
if err is JsonDecodeError {
assert err.line == line
assert err.character == character
assert utf8.validate_str(err.context), err.context
assert err.context == context
} else {
assert false, err.msg()
}
return
}
assert false, 'expected a typed JSON5 decoding error'
}
fn test_json5_typed_error_preview_trims_prefix_by_code_points() {
for ch in ['é', '界', '🎼'] {
input := "{'" + ch.repeat(75) + "': false}"
context := '...' + ch.repeat(47) + "': \\e[31mfalse\\e[0m}\n" +
' '.repeat(53) + '\\e[31m~~~~~\\e[0m'
assert_json5_decode_error_context[map[string]int](input, 1, 81, context)
}
input := '{' + 'é'.repeat(75) + ': false}'
context := '...' + 'é'.repeat(48) + ': \\e[31mfalse\\e[0m}\n' +
' '.repeat(53) + '\\e[31m~~~~~\\e[0m'
assert_json5_decode_error_context[map[string]int](input, 1, 79, context)
assert_json5_decode_error_context[map[string]int]('\uFEFF' + input, 1, 79, context)
assert_json5_decode_error_context[int]('\uFEFFfalse', 1, 1,
'\\e[31mfalse\\e[0m\n\\e[31m~~~~~\\e[0m')
}
fn test_json5_typed_error_preview_prefix_cutoff_boundary() {
for count in [44, 45, 46] {
input := "{'" + 'é'.repeat(count) + "': false}"
prefix := if count == 46 { "...'" } else { "{'" }
columns := if count == 46 { 53 } else { count + 5 }
context := prefix + 'é'.repeat(count) + "': \\e[31mfalse\\e[0m}\n" +
' '.repeat(columns) + '\\e[31m~~~~~\\e[0m'
assert_json5_decode_error_context[map[string]int](input, 1, count + 6, context)
}
}
fn test_json5_typed_error_preview_highlight_and_suffix_end_on_rune_boundaries() {
for ch in ['é', '界', '🎼'] {
input := "'" + ch.repeat(60) + "'"
context := "\\e[31m'" + ch.repeat(49) + '\\e[0m' + ch.repeat(5) + '\n' +
'\\e[31m' + '~'.repeat(50) + '\\e[0m'
assert_json5_decode_error_context[int](input, 1, 1, context)
trailing := 'false/*' + ch.repeat(4) + '*/'
trailing_context := '\\e[31mfalse\\e[0m/*' + ch.repeat(3) + '\n\\e[31m~~~~~\\e[0m'
assert_json5_decode_error_context[int](trailing, 1, 1, trailing_context)
}
}
fn test_json5_typed_error_preview_tabs_use_visible_padding() {
for count in [1, 6, 7, 64] {
cutoff := count > 6
padding := if cutoff { 51 } else { count * 8 }
prefix := if cutoff { '...' } else { '' }
context := prefix + ' '.repeat(if cutoff { 48 } else { count * 8 }) +
'\\e[31mfalse\\e[0m\n' + ' '.repeat(padding) + '\\e[31m~~~~~\\e[0m'
assert_json5_decode_error_context[int]('\t'.repeat(count) + 'false', 1, count * 8 + 1,
context)
}
input := '\t'.repeat(7) + ' /*é*/ false'
context := '...' + ' '.repeat(40) + ' /*é*/ \\e[31mfalse\\e[0m\n' +
' '.repeat(50) + '\\e[31m~~~~~\\e[0m'
assert_json5_decode_error_context[int](input, 1, 64, context)
assert_json5_decode_error_context[int]("'é\tz'", 1, 1,
"\\e[31m'é z'\\e[0m\n\\e[31m" + '~'.repeat(12) + '\\e[0m')
}
fn test_json5_typed_error_preview_stays_on_source_line() {
for separator in ['\n', '\r', '\r\n', '\u2028', '\u2029'] {
input := '// previous' + separator + "{'" + 'é'.repeat(75) + "': false}" +
separator + '// next'
context := '...' + 'é'.repeat(47) + "': \\e[31mfalse\\e[0m}\n" +
' '.repeat(53) + '\\e[31m~~~~~\\e[0m'
assert_json5_decode_error_context[map[string]int](input, 2, 81, context)
assert_json5_decode_error_context[int]('{' + separator + 'name: 1}', 1, 1,
'\\e[31m{\\e[0m\n\\e[31m~\\e[0m')
}
for separator in ['\u2028', '\u2029'] {
assert_json5_decode_error_context[int]("'é" + separator + "rest'", 1, 1,
"\\e[31m'é\\e[0m\n\\e[31m~~\\e[0m")
}
}
fn test_json5_typed_error_after_comments_uses_source_line_and_context() {
input := "// heading\n{\n name: 'V',\n count: /* note */ false,\n}"
decode[Json5ErrorFields](input, json5: true) or {
if err is JsonDecodeError {
assert err.line == 4
assert err.character == 21
assert err.context.starts_with(' count: /* note */ \\e[31mfalse\\e[0m,')
assert !err.context.contains('"count"')
} else {
assert false, err.msg()
}
return
}
assert false, 'expected a typed JSON5 decoding error'
}
fn test_json5_typed_error_after_normalized_strings_and_keys() {
input := "{name: '\\x41\\u0042', count: true,}"
position := input.index('true') or { panic('missing value') }
assert_json5_decode_error[Json5ErrorFields](input, 1, position + 1, 'true')
quoted := "/* leading */ '\\x31'"
assert_json5_decode_error[int](quoted, 1, 15, "'\\x31'")
}
fn test_json5_typed_error_preserves_container_source_span() {
input := '// heading\n { items: [1, /* note */ 2,], }'
assert_json5_decode_error[[]int](input, 2, 3, '{ items: [1, /* note */ 2,], }')
assert_json5_decode_error[map[string]int](input, 2, 12, '[1, /* note */ 2,]')
assert_json5_decode_error[[]int]('// heading\n[1, /* note */ false,]', 2, 16, 'false')
}
fn test_json5_typed_error_preserves_unicode_and_bom_byte_offsets() {
input := "\uFEFF{ 'ключ': '🎼', count: false, }"
position := input.index('false') or { panic('missing value') }
assert_json5_decode_error[Json5ErrorFields](input, 1, input[3..position].runes().len + 1,
'false')
assert_json5_decode_error[int]('\uFEFF\u00A0false', 1, 2, 'false')
assert_json5_decode_error[int]('// heading\r\n\tfalse', 2, 9, 'false')
}
fn test_json5_typed_error_columns_count_code_points() {
assert_json5_decode_error[map[string]int]('{é: false}', 1, 5, 'false')
assert_json5_decode_error[map[string]int]('{界: false}', 1, 5, 'false')
assert_json5_decode_error[map[string]int]('{e\u0301: false}', 1, 6, 'false')
assert_json5_decode_error[map[string]int]("{'🎼': false}", 1, 7, 'false')
assert_json5_decode_error[map[string]int]('{"é🎼": false}', 1, 8, 'false')
assert_json5_decode_error[int]('/* 🎼 é */ false', 1, 11, 'false')
assert_json5_decode_error[map[string]int](r'{\u00e9: false}', 1, 10, 'false')
assert_json5_decode_error[map[string]int]('{\té:\tfalse}', 1, 20, 'false')
for separator in ['\n', '\r', '\r\n', '\u2028', '\u2029'] {
input := '// 🎼 é' + separator + '{é: false}'
assert_json5_decode_error[map[string]int](input, 2, 5, 'false')
}
}
fn test_json5_error_source_mapping_is_independent_of_reused_token_storage() {
mut buffer := DecodeBuffer{}
for input in ['// heading\n{count: false,}', "/* heading */ {name:'\\x41',count:false}"] {
decode_reuse[Json5ErrorFields](input, mut buffer, json5: true) or {
if err is JsonDecodeError {
assert err.context.contains('count:')
assert err.context.contains('\\e[31mfalse\\e[0m')
} else {
assert false, err.msg()
}
assert decode_reuse[Json5ErrorFields]("{name:'valid',count:1}", mut buffer,
json5: true
)!.count == 1
continue
}
assert false, 'expected a typed JSON5 decoding error'
}
}
fn test_json5_typed_error_counts_each_line_terminator_and_limits_context() {
for separator in ['\n', '\r', '\r\n', '\u2028', '\u2029'] {
input := '// heading' + separator + '/* another line */' + separator +
'\tfalse' + separator + '/* following line */'
decode[int](input, json5: true) or {
if err is JsonDecodeError {
assert err.line == 3
assert err.character == 9
assert err.context.starts_with(' \\e[31mfalse\\e[0m\n')
assert !err.context.contains('heading')
assert !err.context.contains('another line')
assert !err.context.contains('following line')
} else {
assert false, err.msg()
}
continue
}
assert false, 'expected a typed JSON5 decoding error'
}
assert_json5_decode_error[int]('// heading\r\n\r\u2028\u2029false', 5, 1, 'false')
}