| 1 | // Instructions for developers: |
| 2 | // The actual tests and data can be obtained by doing: |
| 3 | // `git clone -n https://github.com/toml-lang/toml-test.git vlib/toml/tests/testdata/toml_lang` |
| 4 | // `git -C vlib/toml/tests/testdata/toml_lang reset --hard 229ce2e |
| 5 | // See also the CI toml tests |
| 6 | import os |
| 7 | import toml |
| 8 | import toml.ast |
| 9 | import x.json2 |
| 10 | |
| 11 | const test_root = os.join_path(@VEXEROOT, 'vlib/toml/tests/testdata/toml_lang/tests') |
| 12 | const test_files_file = os.join_path(test_root, 'files-toml-1.0.0') |
| 13 | |
| 14 | const hide_oks = os.getenv('VTEST_HIDE_OK') == '1' |
| 15 | const no_jq = os.getenv('VNO_JQ') == '1' |
| 16 | |
| 17 | // Kept for easier lookup and handling of future updates to the tests. |
| 18 | // NOTE: entries in this list, except `do_not_remove`, are valid TOML that the parser should work with, but currently does not. |
| 19 | const valid_exceptions = [ |
| 20 | 'do_not_remove', |
| 21 | ] |
| 22 | // NOTE: entries in this list, except `do_not_remove`, are tests of invalid TOML that should have the parser fail, but currently does not. |
| 23 | const invalid_exceptions = [ |
| 24 | 'do_not_remove', |
| 25 | ] |
| 26 | const valid_value_exceptions = [ |
| 27 | 'do_not_remove', |
| 28 | ] |
| 29 | const jq_not_equal = [ |
| 30 | 'do_not_remove', |
| 31 | ] |
| 32 | |
| 33 | const jq = os.find_abs_path_of_executable('jq') or { '' } |
| 34 | const compare_work_dir_root = os.join_path(os.vtmp_dir(), 'toml_toml_lang') |
| 35 | // From: https://stackoverflow.com/a/38266731/1904615 |
| 36 | const jq_normalize = r'# Apply f to composite entities recursively using keys[], and to atoms |
| 37 | def sorted_walk(f): |
| 38 | . as $in |
| 39 | | if type == "object" then |
| 40 | reduce keys[] as $key |
| 41 | ( {}; . + { ($key): ($in[$key] | sorted_walk(f)) } ) | f |
| 42 | elif type == "array" then map( sorted_walk(f) ) | f |
| 43 | else f |
| 44 | end; |
| 45 | |
| 46 | def normalize: sorted_walk(if type == "array" then sort else . end); |
| 47 | |
| 48 | normalize' |
| 49 | |
| 50 | fn run(args []string) !string { |
| 51 | res := os.execute(args.join(' ')) |
| 52 | if res.exit_code != 0 { |
| 53 | return error('${args[0]} failed with return code ${res.exit_code}.\n${res.output}') |
| 54 | } |
| 55 | return res.output |
| 56 | } |
| 57 | |
| 58 | // test_toml_lang_tomltest run though 'testdata/toml_lang/toml-test/*' if found. |
| 59 | fn test_toml_lang_tomltest() { |
| 60 | eprintln('> running ${@LOCATION}') |
| 61 | if !os.is_dir(test_root) { |
| 62 | println('No test data directory found in "${test_root}"') |
| 63 | assert true |
| 64 | return |
| 65 | } |
| 66 | test_files_list := os.read_lines(test_files_file) or { |
| 67 | panic('Could not read "${test_files_file}" with test files list. It should reside in "${test_root}": ${err.msg()}') |
| 68 | } |
| 69 | |
| 70 | valid_folder := 'valid' |
| 71 | valid_test_files := |
| 72 | test_files_list.filter(it.starts_with('valid/') && it.ends_with('.toml')).map(it.replace('\\', '/')) |
| 73 | invalid_folder := 'invalid' |
| 74 | invalid_test_files := |
| 75 | test_files_list.filter(it.starts_with('invalid/') && it.ends_with('.toml')).map(it.replace('\\', '/')) |
| 76 | |
| 77 | assert valid_test_files.len > 0, 'Expected a list of *valid* test files' |
| 78 | assert invalid_test_files.len > 0, 'Expected a list of *invalid* test files' |
| 79 | |
| 80 | assert os.is_file(os.join_path(test_root, valid_test_files[0])), 'Expected at least one file from the valid list to be present' |
| 81 | assert os.is_file(os.join_path(test_root, invalid_test_files[0])), 'Expected at least one file from the invalid list to present' |
| 82 | |
| 83 | println('\nTesting ${valid_test_files.len} valid TOML files...') |
| 84 | mut valid := 0 |
| 85 | mut e := 0 |
| 86 | for i, relative_valid_test_file in valid_test_files { |
| 87 | relative := relative_valid_test_file.all_after(valid_folder).trim_left('/') |
| 88 | valid_test_file := os.join_path(test_root, relative_valid_test_file) |
| 89 | if relative in valid_exceptions { |
| 90 | e++ |
| 91 | idx := valid_exceptions.index(relative) + 1 |
| 92 | println('SKIP [${i + 1}/${valid_test_files.len}] "${valid_test_file}" VALID EXCEPTION [${idx}/${valid_exceptions.len}]...') |
| 93 | continue |
| 94 | } |
| 95 | // eprintln('>>> trying to parse: ${valid_test_file} | relative: ${relative}') |
| 96 | toml_doc := toml.parse_file(valid_test_file) or { |
| 97 | eprintln('>>> error while parsing: ${valid_test_file}') |
| 98 | panic(err) |
| 99 | } |
| 100 | valid++ |
| 101 | if !hide_oks { |
| 102 | println('OK [${i + 1}/${valid_test_files.len}] "${valid_test_file}"...') |
| 103 | } |
| 104 | } |
| 105 | println('${valid}/${valid_test_files.len} TOML files were parsed correctly') |
| 106 | if valid_exceptions.len > 0 { |
| 107 | println('TODO Skipped parsing of ${valid_exceptions.len} valid TOML files...') |
| 108 | } |
| 109 | |
| 110 | // If the command-line tool `jq` is installed, value tests can be run as well. |
| 111 | if jq != '' && !no_jq { |
| 112 | println('\nTesting value output of ${valid_test_files.len} valid TOML files using "${jq}"...') |
| 113 | |
| 114 | if os.exists(compare_work_dir_root) { |
| 115 | os.rmdir_all(compare_work_dir_root)! |
| 116 | } |
| 117 | os.mkdir_all(compare_work_dir_root)! |
| 118 | |
| 119 | jq_normalize_path := os.join_path(compare_work_dir_root, 'normalize.jq') |
| 120 | os.write_file(jq_normalize_path, jq_normalize)! |
| 121 | |
| 122 | valid = 0 |
| 123 | e = 0 |
| 124 | for i, relative_valid_test_file in valid_test_files { |
| 125 | relative := relative_valid_test_file.all_after(valid_folder).trim_left('/') |
| 126 | valid_test_file := os.join_path(test_root, relative_valid_test_file) |
| 127 | // Skip the file if we know it can't be parsed or we know that the value retrieval needs work. |
| 128 | if relative in valid_exceptions { |
| 129 | e++ |
| 130 | idx := valid_exceptions.index(relative) + 1 |
| 131 | println('SKIP [${i + 1}/${valid_test_files.len}] "${valid_test_file}" VALID EXCEPTION [${idx}/${valid_exceptions.len}]...') |
| 132 | continue |
| 133 | } |
| 134 | if relative in valid_value_exceptions { |
| 135 | e++ |
| 136 | idx := valid_value_exceptions.index(relative) + 1 |
| 137 | println('SKIP [${i + 1}/${valid_test_files.len}] "${valid_test_file}" VALID VALUE EXCEPTION [${idx}/${valid_value_exceptions.len}]...') |
| 138 | continue |
| 139 | } |
| 140 | |
| 141 | if !hide_oks { |
| 142 | println('OK [${i + 1}/${valid_test_files.len}] "${valid_test_file}"...') |
| 143 | } |
| 144 | toml_doc := toml.parse_file(valid_test_file)! |
| 145 | // eprintln(' relative: ${relative} parsed') |
| 146 | |
| 147 | v_toml_json_path := os.join_path(compare_work_dir_root, |
| 148 | |
| 149 | os.file_name(valid_test_file).all_before_last('.') + '.v.json') |
| 150 | bs_toml_json_path := os.join_path(compare_work_dir_root, |
| 151 | |
| 152 | os.file_name(valid_test_file).all_before_last('.') + '.json') |
| 153 | |
| 154 | os.write_file(v_toml_json_path, to_toml_lang(toml_doc.ast.table))! |
| 155 | |
| 156 | bs_json := os.read_file(valid_test_file.all_before_last('.') + '.json')! |
| 157 | |
| 158 | os.write_file(bs_toml_json_path, bs_json)! |
| 159 | |
| 160 | v_normalized_json := run([jq, '-S', '-f "${jq_normalize_path}"', v_toml_json_path]) or { |
| 161 | contents := os.read_file(v_toml_json_path)! |
| 162 | panic(err.msg() + '\n${contents}') |
| 163 | } |
| 164 | bs_normalized_json := run([jq, '-S', '-f "${jq_normalize_path}"', bs_toml_json_path]) or { |
| 165 | contents := os.read_file(v_toml_json_path)! |
| 166 | panic(err.msg() + '\n${contents}') |
| 167 | } |
| 168 | |
| 169 | if relative in jq_not_equal { |
| 170 | e++ |
| 171 | eprintln('>>> skipped: relative: ${relative} in jq_not_equal, bs_normalized_json != bs_normalized_json') |
| 172 | continue |
| 173 | } |
| 174 | |
| 175 | if bs_normalized_json != v_normalized_json { |
| 176 | e++ |
| 177 | eprintln('>>> error: relative: ${relative}, bs_normalized_json != bs_normalized_json') |
| 178 | continue |
| 179 | } |
| 180 | |
| 181 | assert bs_normalized_json == v_normalized_json |
| 182 | valid++ |
| 183 | } |
| 184 | } else { |
| 185 | println('> Skipping json conversion tests, since jq: ${jq} | no_jq: ${no_jq}') |
| 186 | } |
| 187 | println('${valid}/${valid_test_files.len} TOML files were parsed correctly and value checked') |
| 188 | if valid_value_exceptions.len > 0 { |
| 189 | println('TODO Skipped value checks of ${valid_value_exceptions.len} valid TOML files...') |
| 190 | } |
| 191 | |
| 192 | println('\nTesting ${invalid_test_files.len} invalid TOML files...') |
| 193 | mut invalid := 0 |
| 194 | e = 0 |
| 195 | for i, relative_invalid_test_file in invalid_test_files { |
| 196 | relative := relative_invalid_test_file.all_after(invalid_folder).trim_left('/') |
| 197 | invalid_test_file := os.join_path(test_root, relative_invalid_test_file) |
| 198 | if relative in invalid_exceptions { |
| 199 | e++ |
| 200 | idx := invalid_exceptions.index(relative) + 1 |
| 201 | println('SKIP [${i + 1}/${invalid_test_files.len}] "${invalid_test_file}" INVALID EXCEPTION [${idx}/${invalid_exceptions.len}]...') |
| 202 | continue |
| 203 | } |
| 204 | if !hide_oks { |
| 205 | println('OK [${i + 1}/${invalid_test_files.len}] "${invalid_test_file}"...') |
| 206 | } |
| 207 | if toml_doc := toml.parse_file(invalid_test_file) { |
| 208 | content_that_should_have_failed := os.read_file(invalid_test_file)! |
| 209 | println(' This TOML from relative: ${relative}, invalid_test_file: ${invalid_test_file}, should have failed:\n${'-'.repeat(100)}\n${content_that_should_have_failed}\n${'-'.repeat(100)}') |
| 210 | assert false |
| 211 | } else { |
| 212 | if !hide_oks { |
| 213 | println(' ${err.msg()}') |
| 214 | } |
| 215 | assert true |
| 216 | } |
| 217 | invalid++ |
| 218 | } |
| 219 | println('${invalid}/${invalid_test_files.len} invalid TOML files correctly had parsing errors') |
| 220 | if invalid_exceptions.len > 0 { |
| 221 | println('TODO Skipped parsing of ${invalid_exceptions.len} invalid TOML files...') |
| 222 | } |
| 223 | } |
| 224 | |
| 225 | // to_toml_lang returns a toml-lang compatible json string converted from the `value` ast.Value. |
| 226 | fn to_toml_lang(value ast.Value) string { |
| 227 | match value { |
| 228 | ast.Quoted { |
| 229 | json_text := json2.Any(value.text).json_str() |
| 230 | return '{ "type": "string", "value": ${json_text} }' |
| 231 | } |
| 232 | ast.DateTime { |
| 233 | // Normalization for json |
| 234 | json_text := json2.Any(value.text).json_str().to_upper().replace(' ', 'T') |
| 235 | |
| 236 | // Note: Since encoding strings in JSON now automatically includes quotes, |
| 237 | // I added a somewhat a workaround by adding an ending quote in order to |
| 238 | // recognize properly the date time type. - Ned |
| 239 | typ := if json_text.ends_with('Z"') || json_text.all_after('T').contains('-') |
| 240 | || json_text.all_after('T').contains('+') { |
| 241 | 'datetime' |
| 242 | } else { |
| 243 | 'datetime-local' |
| 244 | } |
| 245 | return '{ "type": "${typ}", "value": ${json_text} }' |
| 246 | } |
| 247 | ast.Date { |
| 248 | json_text := json2.Any(value.text).json_str() |
| 249 | return '{ "type": "date-local", "value": ${json_text} }' |
| 250 | } |
| 251 | ast.Time { |
| 252 | json_text := json2.Any(value.text).json_str() |
| 253 | return '{ "type": "time-local", "value": ${json_text} }' |
| 254 | } |
| 255 | ast.Bool { |
| 256 | json_text := json2.Any(value.text.bool()).json_str() |
| 257 | return '{ "type": "bool", "value": "${json_text}" }' |
| 258 | } |
| 259 | ast.Null { |
| 260 | json_text := json2.Any(value.text).json_str() |
| 261 | return '{ "type": "null", "value": ${json_text} }' |
| 262 | } |
| 263 | ast.Number { |
| 264 | if value.text.contains('inf') || value.text.contains('nan') { |
| 265 | return '{ "type": "float", "value": "${value.text}" }' |
| 266 | } |
| 267 | if !value.text.starts_with('0x') |
| 268 | && (value.text.contains('.') || value.text.to_lower().contains('e')) { |
| 269 | mut val := '${value.f64()}'.replace('.e+', '.0e') // JSON notation |
| 270 | if !val.contains('.') && val != '0' { // JSON notation |
| 271 | val += '.0' |
| 272 | } |
| 273 | // Since https://github.com/vlang/v/pull/16079 V's string conversion of a zero (0) will |
| 274 | // output "0.0" for float types - the JSON test suite data, however, expects "0" for floats |
| 275 | // The following is a correction for that inconsistency |
| 276 | if val == '0.0' { |
| 277 | val = '0' |
| 278 | } |
| 279 | return '{ "type": "float", "value": "${val}" }' |
| 280 | } |
| 281 | return '{ "type": "integer", "value": "${value.i64()}" }' |
| 282 | } |
| 283 | map[string]ast.Value { |
| 284 | mut str := '{ ' |
| 285 | for key, val in value { |
| 286 | json_key := json2.Any(key).json_str() |
| 287 | str += ' ${json_key}: ${to_toml_lang(val)},' |
| 288 | } |
| 289 | str = str.trim_right(',') |
| 290 | str += ' }' |
| 291 | return str |
| 292 | } |
| 293 | []ast.Value { |
| 294 | mut str := '[ ' |
| 295 | for val in value { |
| 296 | str += ' ${to_toml_lang(val)},' |
| 297 | } |
| 298 | str = str.trim_right(',') |
| 299 | str += ' ]\n' |
| 300 | return str |
| 301 | } |
| 302 | } |
| 303 | |
| 304 | return '<error>' |
| 305 | } |
| 306 | |