| 1 | import os |
| 2 | import toml |
| 3 | import toml.ast |
| 4 | import x.json2 |
| 5 | |
| 6 | const hide_oks = os.getenv('VTEST_HIDE_OK') == '1' |
| 7 | |
| 8 | // Can be set to `true` to process tests that stress test the parser |
| 9 | // by having large data amounts - these pass - but slow down the test run |
| 10 | const do_large_files = os.getenv('VTEST_TOML_DO_LARGE_FILES') == '1' |
| 11 | |
| 12 | // Can be set to `true` to process tests that triggers a slow conversion |
| 13 | // process that uses `python` to convert from YAML to JSON. |
| 14 | const do_yaml_conversion = os.getenv('VTEST_TOML_DO_YAML_CONVERSION') == '1' |
| 15 | |
| 16 | // Instructions for developers: |
| 17 | // The actual tests and data can be obtained by doing: |
| 18 | // `git clone --depth 1 https://github.com/iarna/toml-spec-tests.git vlib/toml/tests/testdata/iarna/toml-test` |
| 19 | // See also the CI toml tests |
| 20 | // Kept for easier handling of future updates to the tests |
| 21 | const valid_exceptions = []string{} |
| 22 | const invalid_exceptions = []string{} |
| 23 | |
| 24 | const valid_value_exceptions = [ |
| 25 | 'values/spec-date-time-3.toml', |
| 26 | 'values/spec-date-time-4.toml', |
| 27 | 'values/spec-readme-example.toml', |
| 28 | 'values/spec-date-time-6.toml', |
| 29 | 'values/spec-date-time-5.toml', |
| 30 | 'values/spec-date-time-1.toml', |
| 31 | 'values/spec-date-time-2.toml', |
| 32 | 'values/qa-table-inline-nested-1000.toml', |
| 33 | 'values/qa-array-inline-nested-1000.toml', |
| 34 | ] |
| 35 | |
| 36 | const yaml_value_exceptions = [ |
| 37 | 'values/spec-float-5.toml', // YAML: "1e6", V: 1000000 |
| 38 | 'values/spec-float-9.toml', // YAML: "-0e0", V: 0 |
| 39 | 'values/spec-float-6.toml', // YAML: "-2E-2", V: -0.02 |
| 40 | 'values/spec-float-4.toml', // YAML: "5e+22", V: 50000000000000004000000 |
| 41 | ] |
| 42 | |
| 43 | const jq = os.find_abs_path_of_executable('jq') or { '' } |
| 44 | const python = os.find_abs_path_of_executable('python') or { '' } |
| 45 | const compare_work_dir_root = os.join_path(os.vtmp_dir(), 'toml_iarna') |
| 46 | // From: https://stackoverflow.com/a/38266731/1904615 |
| 47 | const jq_normalize = r'# Apply f to composite entities recursively using keys[], and to atoms |
| 48 | def sorted_walk(f): |
| 49 | . as $in |
| 50 | | if type == "object" then |
| 51 | reduce keys[] as $key |
| 52 | ( {}; . + { ($key): ($in[$key] | sorted_walk(f)) } ) | f |
| 53 | elif type == "array" then map( sorted_walk(f) ) | f |
| 54 | else f |
| 55 | end; |
| 56 | |
| 57 | def normalize: sorted_walk(if type == "array" then sort else . end); |
| 58 | |
| 59 | normalize' |
| 60 | |
| 61 | fn run(args []string) !string { |
| 62 | res := os.execute(args.join(' ')) |
| 63 | if res.exit_code != 0 { |
| 64 | return error('${args[0]} failed with return code ${res.exit_code}.\n${res.output}') |
| 65 | } |
| 66 | return res.output |
| 67 | } |
| 68 | |
| 69 | // test_iarna_toml_spec_tests run though 'testdata/iarna/toml-test/*' if found. |
| 70 | fn test_iarna_toml_spec_tests() { |
| 71 | this_file := @FILE |
| 72 | test_root := os.join_path(os.dir(this_file), 'testdata', 'iarna') |
| 73 | if os.is_dir(test_root) { |
| 74 | valid_test_files := os.walk_ext(os.join_path(test_root, 'values'), '.toml') |
| 75 | println('Testing ${valid_test_files.len} valid TOML files...') |
| 76 | mut valid := 0 |
| 77 | mut e := 0 |
| 78 | for i, valid_test_file in valid_test_files { |
| 79 | mut relative := valid_test_file.all_after('iarna').trim_left(os.path_separator) |
| 80 | $if windows { |
| 81 | relative = relative.replace('/', '\\') |
| 82 | } |
| 83 | |
| 84 | if !do_large_files && valid_test_file.contains('qa-') { |
| 85 | e++ |
| 86 | println('SKIP [${i + 1}/${valid_test_files.len}] "${valid_test_file}" LARGE FILE...') |
| 87 | continue |
| 88 | } |
| 89 | |
| 90 | if relative in valid_exceptions { |
| 91 | e++ |
| 92 | idx := valid_exceptions.index(relative) + 1 |
| 93 | println('SKIP [${i + 1}/${valid_test_files.len}] "${valid_test_file}" VALID EXCEPTION [${idx}/${valid_exceptions.len}]...') |
| 94 | continue |
| 95 | } |
| 96 | |
| 97 | if !hide_oks { |
| 98 | println('OK [${i + 1}/${valid_test_files.len}] "${valid_test_file}"...') |
| 99 | } |
| 100 | toml_doc := toml.parse_file(valid_test_file)! |
| 101 | valid++ |
| 102 | } |
| 103 | println('${valid}/${valid_test_files.len} TOML files were parsed correctly') |
| 104 | if valid_exceptions.len > 0 { |
| 105 | println('TODO Skipped parsing of ${e} valid TOML files...') |
| 106 | } |
| 107 | |
| 108 | // If the command-line tool `jq` is installed, value tests can be run as well. |
| 109 | if jq != '' { |
| 110 | println('Testing value output of ${valid_test_files.len} valid TOML files using "${jq}"...') |
| 111 | |
| 112 | if os.exists(compare_work_dir_root) { |
| 113 | os.rmdir_all(compare_work_dir_root)! |
| 114 | } |
| 115 | os.mkdir_all(compare_work_dir_root)! |
| 116 | |
| 117 | jq_normalize_path := os.join_path(compare_work_dir_root, 'normalize.jq') |
| 118 | os.write_file(jq_normalize_path, jq_normalize)! |
| 119 | |
| 120 | valid = 0 |
| 121 | e = 0 |
| 122 | for i, valid_test_file in valid_test_files { |
| 123 | mut relative := valid_test_file.all_after('iarna').trim_left(os.path_separator) |
| 124 | $if windows { |
| 125 | relative = relative.replace('/', '\\') |
| 126 | } |
| 127 | |
| 128 | // Skip the file if we know it can't be parsed or we know that the value retrieval needs work. |
| 129 | if relative in valid_exceptions { |
| 130 | e++ |
| 131 | idx := valid_exceptions.index(relative) + 1 |
| 132 | println('SKIP [${i + 1}/${valid_test_files.len}] "${valid_test_file}" VALID EXCEPTION [${idx}/${valid_exceptions.len}]...') |
| 133 | continue |
| 134 | } |
| 135 | |
| 136 | if relative in valid_value_exceptions { |
| 137 | e++ |
| 138 | idx := valid_value_exceptions.index(relative) + 1 |
| 139 | println('SKIP [${i + 1}/${valid_test_files.len}] "${valid_test_file}" VALID VALUE EXCEPTION [${idx}/${valid_value_exceptions.len}]...') |
| 140 | continue |
| 141 | } |
| 142 | |
| 143 | valid_test_file_name := os.file_name(valid_test_file).all_before_last('.') |
| 144 | uses_json_format := os.exists(valid_test_file.all_before_last('.') + '.json') |
| 145 | |
| 146 | // Use python to convert the YAML files to json - it yields some inconsistencies |
| 147 | // so we skip some of them |
| 148 | mut converted_from_yaml := false |
| 149 | mut converted_json_path := '' |
| 150 | if !uses_json_format { |
| 151 | $if windows { |
| 152 | println('N/A [${i + 1}/${valid_test_files.len}] "${valid_test_file}"...') |
| 153 | continue |
| 154 | } |
| 155 | if python == '' { |
| 156 | println('N/A [${i + 1}/${valid_test_files.len}] "${valid_test_file}"...') |
| 157 | continue |
| 158 | } |
| 159 | if !do_yaml_conversion || relative in yaml_value_exceptions { |
| 160 | e++ |
| 161 | idx := yaml_value_exceptions.index(relative) + 1 |
| 162 | println('SKIP [${i + 1}/${valid_test_files.len}] "${valid_test_file}" YAML VALUE EXCEPTION [${idx}/${valid_value_exceptions.len}]...') |
| 163 | continue |
| 164 | } |
| 165 | |
| 166 | if !do_large_files && valid_test_file.contains('qa-') { |
| 167 | e++ |
| 168 | println('SKIP [${i + 1}/${valid_test_files.len}] "${valid_test_file}" LARGE FILE...') |
| 169 | continue |
| 170 | } |
| 171 | |
| 172 | iarna_yaml_path := valid_test_file.all_before_last('.') + '.yaml' |
| 173 | if os.exists(iarna_yaml_path) { |
| 174 | converted_json_path = os.join_path(compare_work_dir_root, |
| 175 | '${valid_test_file_name}.yaml.json') |
| 176 | run([python, '-c', |
| 177 | "'import sys, yaml, json; json.dump(yaml.load(sys.stdin, Loader=yaml.FullLoader), sys.stdout, indent=4)'", |
| 178 | '<', iarna_yaml_path, '>', converted_json_path]) or { |
| 179 | contents := os.read_file(iarna_yaml_path)! |
| 180 | // NOTE there's known errors with the python convention method. |
| 181 | // For now we just ignore them as it's a broken tool - not a wrong test-case. |
| 182 | // Uncomment this print to see/check them. |
| 183 | // eprintln(err.msg() + '\n${contents}') |
| 184 | e++ |
| 185 | println('ERR [${i + 1}/${valid_test_files.len}] "${valid_test_file}" EXCEPTION [${e}/${valid_value_exceptions.len}]...') |
| 186 | continue |
| 187 | } |
| 188 | converted_from_yaml = true |
| 189 | } |
| 190 | } |
| 191 | |
| 192 | if !hide_oks { |
| 193 | println('OK [${i + 1}/${valid_test_files.len}] "${valid_test_file}"...') |
| 194 | } |
| 195 | toml_doc := toml.parse_file(valid_test_file)! |
| 196 | |
| 197 | v_toml_json_path := os.join_path(compare_work_dir_root, |
| 198 | '${valid_test_file_name}.v.json') |
| 199 | iarna_toml_json_path := os.join_path(compare_work_dir_root, |
| 200 | '${valid_test_file_name}.json') |
| 201 | |
| 202 | os.write_file(v_toml_json_path, to_iarna(toml_doc.ast.table, converted_from_yaml))! |
| 203 | |
| 204 | if converted_json_path == '' { |
| 205 | converted_json_path = valid_test_file.all_before_last('.') + '.json' |
| 206 | } |
| 207 | iarna_json := os.read_file(converted_json_path)! |
| 208 | os.write_file(iarna_toml_json_path, iarna_json)! |
| 209 | |
| 210 | v_normalized_json := run([jq, '-S', '-f "${jq_normalize_path}"', v_toml_json_path]) or { |
| 211 | contents := os.read_file(v_toml_json_path)! |
| 212 | panic(err.msg() + '\n${contents}') |
| 213 | } |
| 214 | cmd := [jq, '-S', '-f "${jq_normalize_path}"', iarna_toml_json_path] |
| 215 | iarna_normalized_json := run(cmd) or { |
| 216 | contents := os.read_file(v_toml_json_path)! |
| 217 | panic(err.msg() + '\n${contents}\n\ncmd: ${cmd.join(' ')}') |
| 218 | } |
| 219 | |
| 220 | assert iarna_normalized_json == v_normalized_json |
| 221 | |
| 222 | valid++ |
| 223 | } |
| 224 | println('${valid}/${valid_test_files.len} TOML files were parsed correctly and value checked') |
| 225 | if valid_value_exceptions.len > 0 { |
| 226 | println('TODO Skipped value checks of ${e} valid TOML files...') |
| 227 | } |
| 228 | } |
| 229 | |
| 230 | invalid_test_files := os.walk_ext(os.join_path(test_root, 'errors'), '.toml') |
| 231 | println('Testing ${invalid_test_files.len} invalid TOML files...') |
| 232 | mut invalid := 0 |
| 233 | e = 0 |
| 234 | for i, invalid_test_file in invalid_test_files { |
| 235 | mut relative := invalid_test_file.all_after('iarna').trim_left(os.path_separator) |
| 236 | $if windows { |
| 237 | relative = relative.replace('/', '\\') |
| 238 | } |
| 239 | if relative in invalid_exceptions { |
| 240 | e++ |
| 241 | idx := invalid_exceptions.index(relative) + 1 |
| 242 | println('SKIP [${i + 1}/${invalid_test_files.len}] "${invalid_test_file}" INVALID EXCEPTION [${idx}/${invalid_exceptions.len}]...') |
| 243 | continue |
| 244 | } |
| 245 | if !hide_oks { |
| 246 | println('OK [${i + 1}/${invalid_test_files.len}] "${invalid_test_file}"...') |
| 247 | } |
| 248 | if toml_doc := toml.parse_file(invalid_test_file) { |
| 249 | content_that_should_have_failed := os.read_file(invalid_test_file)! |
| 250 | println(' This TOML should have failed:\n${'-'.repeat(40)}\n${content_that_should_have_failed}\n${'-'.repeat(40)}') |
| 251 | assert false |
| 252 | } else { |
| 253 | if !hide_oks { |
| 254 | println(' ${err.msg()}') |
| 255 | } |
| 256 | assert true |
| 257 | } |
| 258 | invalid++ |
| 259 | } |
| 260 | println('${invalid}/${invalid_test_files.len} TOML files were parsed correctly') |
| 261 | if invalid_exceptions.len > 0 { |
| 262 | println('TODO Skipped parsing of ${invalid_exceptions.len} invalid TOML files...') |
| 263 | } |
| 264 | } else { |
| 265 | println('No test data directory found in "${test_root}"') |
| 266 | assert true |
| 267 | } |
| 268 | } |
| 269 | |
| 270 | // to_iarna_time |
| 271 | fn to_iarna_time(time_str string) string { |
| 272 | if time_str.contains('.') { |
| 273 | date_and_time := time_str.all_before('.') |
| 274 | mut ms := time_str.all_after('.') |
| 275 | z := if ms.contains('Z') { 'Z' } else { '' } |
| 276 | ms = ms.replace('Z', '') |
| 277 | if ms.len > 3 { |
| 278 | ms = ms[..3] |
| 279 | } |
| 280 | return date_and_time + '.' + ms + z |
| 281 | } else { |
| 282 | return time_str + '.000' |
| 283 | } |
| 284 | } |
| 285 | |
| 286 | // to_iarna returns a iarna compatible json string converted from the `value` ast.Value. |
| 287 | fn to_iarna(value ast.Value, skip_value_map bool) string { |
| 288 | match value { |
| 289 | ast.Quoted { |
| 290 | json_text := json2.Any(value.text).json_str() |
| 291 | if skip_value_map { |
| 292 | return json_text |
| 293 | } |
| 294 | return '{ "type": "string", "value": ${json_text} }' |
| 295 | } |
| 296 | ast.DateTime { |
| 297 | // Normalization for json |
| 298 | mut json_text := json2.Any(value.text).json_str().to_upper().replace(' ', 'T') |
| 299 | typ := if json_text.ends_with('Z"') || json_text.all_after('T').contains('-') |
| 300 | || json_text.all_after('T').contains('+') { |
| 301 | 'datetime' |
| 302 | } else { |
| 303 | 'datetime-local' |
| 304 | } |
| 305 | // NOTE test suite inconsistency. |
| 306 | // It seems it's implementation specific how time and |
| 307 | // date-time values are represented in detail. For now we follow the toml-lang format |
| 308 | // that expands to 6 digits which is also a valid RFC 3339 representation. |
| 309 | json_text = to_iarna_time(json_text[1..json_text.len - 1]) |
| 310 | if skip_value_map { |
| 311 | return json_text |
| 312 | } |
| 313 | return '{ "type": "${typ}", "value": "${json_text}" }' |
| 314 | } |
| 315 | ast.Date { |
| 316 | json_text := json2.Any(value.text).json_str() |
| 317 | if skip_value_map { |
| 318 | return json_text |
| 319 | } |
| 320 | return '{ "type": "date", "value": ${json_text} }' |
| 321 | } |
| 322 | ast.Time { |
| 323 | mut json_text := json2.Any(value.text).json_str() |
| 324 | // Note: Removes the quotes of the encoded JSON string - Ned |
| 325 | json_text = to_iarna_time(json_text[1..json_text.len - 1]) |
| 326 | if skip_value_map { |
| 327 | return json_text |
| 328 | } |
| 329 | return '{ "type": "time", "value": "${json_text}" }' |
| 330 | } |
| 331 | ast.Bool { |
| 332 | json_text := json2.Any(value.text.bool()).json_str() |
| 333 | if skip_value_map { |
| 334 | return json_text |
| 335 | } |
| 336 | return '{ "type": "bool", "value": "${json_text}" }' |
| 337 | } |
| 338 | ast.Null { |
| 339 | json_text := json2.Any(value.text).json_str() |
| 340 | if skip_value_map { |
| 341 | return json_text |
| 342 | } |
| 343 | return '{ "type": "null", "value": ${json_text} }' |
| 344 | } |
| 345 | ast.Number { |
| 346 | if value.text.contains('inf') { |
| 347 | mut json_text := |
| 348 | value.text.replace('inf', '1.7976931348623157e+308') // Inconsistency ??? |
| 349 | if skip_value_map { |
| 350 | return '${json_text}' |
| 351 | } |
| 352 | return '{ "type": "float", "value": "${json_text}" }' |
| 353 | } |
| 354 | if value.text.contains('nan') { |
| 355 | mut json_text := 'null' |
| 356 | if skip_value_map { |
| 357 | return '${json_text}' |
| 358 | } |
| 359 | return '{ "type": "float", "value": "${json_text}" }' |
| 360 | } |
| 361 | if !value.text.starts_with('0x') |
| 362 | && (value.text.contains('.') || value.text.to_lower().contains('e')) { |
| 363 | mut val := '${value.f64()}'.replace('.e+', '.0e') // json notation |
| 364 | if !val.contains('.') && val != '0' { // json notation |
| 365 | val += '.0' |
| 366 | } |
| 367 | if skip_value_map { |
| 368 | return '${val}' |
| 369 | } |
| 370 | return '{ "type": "float", "value": "${val}" }' |
| 371 | } |
| 372 | v := value.i64() |
| 373 | if skip_value_map { |
| 374 | return '${v}' |
| 375 | } |
| 376 | return '{ "type": "integer", "value": "${v}" }' |
| 377 | } |
| 378 | map[string]ast.Value { |
| 379 | mut str := '{ ' |
| 380 | for key, val in value { |
| 381 | json_key := json2.Any(key).json_str() |
| 382 | str += ' ${json_key}: ${to_iarna(val, skip_value_map)},' |
| 383 | } |
| 384 | str = str.trim_right(',') |
| 385 | str += ' }' |
| 386 | return str |
| 387 | } |
| 388 | []ast.Value { |
| 389 | mut str := '[ ' |
| 390 | for val in value { |
| 391 | str += ' ${to_iarna(val, skip_value_map)},' |
| 392 | } |
| 393 | str = str.trim_right(',') |
| 394 | str += ' ]\n' |
| 395 | return str |
| 396 | } |
| 397 | } |
| 398 | |
| 399 | return '<error>' |
| 400 | } |
| 401 | |