| 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 | const no_jq = os.getenv('VNO_JQ') == '1' |
| 8 | |
| 9 | // Instructions for developers: |
| 10 | // The actual tests and data can be obtained by doing: |
| 11 | // `git clone -n https://github.com/toml-rs/toml.git vlib/toml/tests/testdata/toml_rs` |
| 12 | // `git -C vlib/toml/tests/testdata/toml_rs reset --hard 9bd454c` |
| 13 | // See also the CI toml tests |
| 14 | // Kept for easier handling of future updates to the tests |
| 15 | const valid_exceptions = [ |
| 16 | 'valid/example-v0.3.0.toml', |
| 17 | 'valid/example-v0.4.0.toml', |
| 18 | 'valid/datetime-truncate.toml', // Not considered valid since RFC 3339 doesn't permit > 6 ms digits ?? |
| 19 | ] |
| 20 | const invalid_exceptions = []string{} |
| 21 | |
| 22 | const valid_value_exceptions = [ |
| 23 | // These have correct values, and should've passed, but the format of arrays is *mixed* in the JSON ?? |
| 24 | 'valid/datetime-truncate.toml', |
| 25 | 'valid/example2.toml', |
| 26 | 'valid/example-v0.4.0.toml', |
| 27 | 'valid/example-v0.3.0.toml', |
| 28 | ] |
| 29 | |
| 30 | // These have correct values, and should've passed as-is, but the format of arrays changes in the JSON ?? |
| 31 | // We account for that here |
| 32 | const use_type_2_arrays = [ |
| 33 | 'valid/table-array-implicit.toml', |
| 34 | 'valid/table-array-many.toml', |
| 35 | 'valid/table-array-one.toml', |
| 36 | 'valid/table-array-nest.toml', |
| 37 | 'valid/table-array-nest-no-keys.toml', |
| 38 | ] |
| 39 | const tests_folder = os.join_path('crates', 'test-suite', 'tests') |
| 40 | const jq = os.find_abs_path_of_executable('jq') or { '' } |
| 41 | const compare_work_dir_root = os.join_path(os.vtmp_dir(), 'toml_toml_rs') |
| 42 | // From: https://stackoverflow.com/a/38266731/1904615 |
| 43 | const jq_normalize = r'# Apply f to composite entities recursively using keys[], and to atoms |
| 44 | def sorted_walk(f): |
| 45 | . as $in |
| 46 | | if type == "object" then |
| 47 | reduce keys[] as $key |
| 48 | ( {}; . + { ($key): ($in[$key] | sorted_walk(f)) } ) | f |
| 49 | elif type == "array" then map( sorted_walk(f) ) | f |
| 50 | else f |
| 51 | end; |
| 52 | |
| 53 | def normalize: sorted_walk(if type == "array" then sort else . end); |
| 54 | |
| 55 | normalize' |
| 56 | |
| 57 | fn run(args []string) !string { |
| 58 | res := os.execute(args.join(' ')) |
| 59 | if res.exit_code != 0 { |
| 60 | return error('${args[0]} failed with return code ${res.exit_code}.\n${res.output}') |
| 61 | } |
| 62 | return res.output |
| 63 | } |
| 64 | |
| 65 | // test_toml_rs_toml_rs run though 'testdata/toml_rs/crates/test-suite/tests/*' if found. |
| 66 | fn test_toml_rs_toml_rs() { |
| 67 | eprintln('> running ${@LOCATION}') |
| 68 | test_root := '${@VEXEROOT}/vlib/toml/tests/testdata/toml_rs/crates/test-suite/tests' |
| 69 | if os.is_dir(test_root) { |
| 70 | valid_test_files := os.walk_ext(os.join_path(test_root, 'valid'), '.toml') |
| 71 | invalid_test_files := os.walk_ext(os.join_path(test_root, 'invalid'), '.toml') |
| 72 | println('Testing ${valid_test_files.len} valid TOML files...') |
| 73 | mut valid := 0 |
| 74 | mut e := 0 |
| 75 | for i, valid_test_file in valid_test_files { |
| 76 | mut relative := valid_test_file.all_after(tests_folder).trim_left(os.path_separator) |
| 77 | $if windows { |
| 78 | relative = relative.replace('/', '\\') |
| 79 | } |
| 80 | |
| 81 | if relative in valid_exceptions { |
| 82 | e++ |
| 83 | idx := valid_exceptions.index(relative) + 1 |
| 84 | println('SKIP [${i + 1}/${valid_test_files.len}] "${valid_test_file}" VALID EXCEPTION [${idx}/${valid_exceptions.len}]...') |
| 85 | continue |
| 86 | } |
| 87 | if !hide_oks { |
| 88 | println('OK [${i + 1}/${valid_test_files.len}] "${valid_test_file}"...') |
| 89 | } |
| 90 | toml_doc := toml.parse_file(valid_test_file)! |
| 91 | valid++ |
| 92 | } |
| 93 | println('${valid}/${valid_test_files.len} TOML files were parsed correctly') |
| 94 | if valid_exceptions.len > 0 { |
| 95 | println('TODO Skipped parsing of ${e} valid TOML files...') |
| 96 | } |
| 97 | |
| 98 | // If the command-line tool `jq` is installed, value tests can be run as well. |
| 99 | if jq != '' && !no_jq { |
| 100 | println('Testing value output of ${valid_test_files.len} valid TOML files using "${jq}"...') |
| 101 | |
| 102 | if os.exists(compare_work_dir_root) { |
| 103 | os.rmdir_all(compare_work_dir_root)! |
| 104 | } |
| 105 | os.mkdir_all(compare_work_dir_root)! |
| 106 | |
| 107 | jq_normalize_path := os.join_path(compare_work_dir_root, 'normalize.jq') |
| 108 | os.write_file(jq_normalize_path, jq_normalize)! |
| 109 | |
| 110 | valid = 0 |
| 111 | e = 0 |
| 112 | for i, valid_test_file in valid_test_files { |
| 113 | mut relative := valid_test_file.all_after(tests_folder).trim_left(os.path_separator) |
| 114 | $if windows { |
| 115 | relative = relative.replace('/', '\\') |
| 116 | } |
| 117 | if !os.exists(valid_test_file.all_before_last('.') + '.json') { |
| 118 | println('N/A [${i + 1}/${valid_test_files.len}] "${valid_test_file}"...') |
| 119 | continue |
| 120 | } |
| 121 | // Skip the file if we know it can't be parsed or we know that the value retrieval needs work. |
| 122 | if relative in valid_exceptions { |
| 123 | e++ |
| 124 | idx := valid_exceptions.index(relative) + 1 |
| 125 | println('SKIP [${i + 1}/${valid_test_files.len}] "${valid_test_file}" VALID EXCEPTION [${idx}/${valid_exceptions.len}]...') |
| 126 | continue |
| 127 | } |
| 128 | if relative in valid_value_exceptions { |
| 129 | e++ |
| 130 | idx := valid_value_exceptions.index(relative) + 1 |
| 131 | println('SKIP [${i + 1}/${valid_test_files.len}] "${valid_test_file}" VALID VALUE EXCEPTION [${idx}/${valid_value_exceptions.len}]...') |
| 132 | continue |
| 133 | } |
| 134 | |
| 135 | if !hide_oks { |
| 136 | println('OK [${i + 1}/${valid_test_files.len}] "${valid_test_file}"...') |
| 137 | } |
| 138 | toml_doc := toml.parse_file(valid_test_file)! |
| 139 | |
| 140 | v_toml_json_path := os.join_path(compare_work_dir_root, |
| 141 | |
| 142 | os.file_name(valid_test_file).all_before_last('.') + '.v.json') |
| 143 | toml_rs_toml_json_path := os.join_path(compare_work_dir_root, |
| 144 | |
| 145 | os.file_name(valid_test_file).all_before_last('.') + '.json') |
| 146 | |
| 147 | mut array_type := 1 |
| 148 | if relative in use_type_2_arrays { |
| 149 | array_type = 2 |
| 150 | } |
| 151 | |
| 152 | os.write_file(v_toml_json_path, to_toml_rs(toml_doc.ast.table, array_type))! |
| 153 | |
| 154 | toml_rs_json := os.read_file(valid_test_file.all_before_last('.') + '.json')! |
| 155 | |
| 156 | os.write_file(toml_rs_toml_json_path, toml_rs_json)! |
| 157 | |
| 158 | v_normalized_json := run([jq, '-S', '-f "${jq_normalize_path}"', v_toml_json_path]) or { |
| 159 | contents := os.read_file(v_toml_json_path)! |
| 160 | panic(err.msg() + '\n${contents}') |
| 161 | } |
| 162 | toml_rs_normalized_json := run([jq, '-S', '-f "${jq_normalize_path}"', |
| 163 | toml_rs_toml_json_path]) or { |
| 164 | contents := os.read_file(v_toml_json_path)! |
| 165 | panic(err.msg() + '\n${contents}') |
| 166 | } |
| 167 | |
| 168 | assert toml_rs_normalized_json == v_normalized_json |
| 169 | |
| 170 | valid++ |
| 171 | } |
| 172 | println('${valid}/${valid_test_files.len} TOML files were parsed correctly and value checked') |
| 173 | if valid_value_exceptions.len > 0 { |
| 174 | println('TODO Skipped value checks of ${e} valid TOML files...') |
| 175 | } |
| 176 | } |
| 177 | |
| 178 | println('Testing ${invalid_test_files.len} invalid TOML files...') |
| 179 | mut invalid := 0 |
| 180 | e = 0 |
| 181 | for i, invalid_test_file in invalid_test_files { |
| 182 | mut relative := invalid_test_file.all_after(tests_folder).trim_left(os.path_separator) |
| 183 | $if windows { |
| 184 | relative = relative.replace('/', '\\') |
| 185 | } |
| 186 | if relative in invalid_exceptions { |
| 187 | e++ |
| 188 | idx := invalid_exceptions.index(relative) + 1 |
| 189 | println('SKIP [${i + 1}/${invalid_test_files.len}] "${invalid_test_file}" INVALID EXCEPTION [${idx}/${invalid_exceptions.len}]...') |
| 190 | continue |
| 191 | } |
| 192 | |
| 193 | if !hide_oks { |
| 194 | println('OK [${i + 1}/${invalid_test_files.len}] "${invalid_test_file}"...') |
| 195 | } |
| 196 | if toml_doc := toml.parse_file(invalid_test_file) { |
| 197 | content_that_should_have_failed := os.read_file(invalid_test_file)! |
| 198 | println(' This TOML should have failed:\n${'-'.repeat(40)}\n${content_that_should_have_failed}\n${'-'.repeat(40)}') |
| 199 | assert false |
| 200 | } else { |
| 201 | if !hide_oks { |
| 202 | println(' ${err.msg()}') |
| 203 | } |
| 204 | assert true |
| 205 | } |
| 206 | invalid++ |
| 207 | } |
| 208 | println('${invalid}/${invalid_test_files.len} TOML files were parsed correctly') |
| 209 | if invalid_exceptions.len > 0 { |
| 210 | println('TODO Skipped parsing of ${invalid_exceptions.len} invalid TOML files...') |
| 211 | } |
| 212 | } else { |
| 213 | println('No test data directory found in "${test_root}"') |
| 214 | assert true |
| 215 | } |
| 216 | } |
| 217 | |
| 218 | // to_toml_rs_time |
| 219 | fn to_toml_rs_time(time_str string) string { |
| 220 | if time_str.contains('.') { |
| 221 | date_and_time := time_str.all_before('.') |
| 222 | mut ms := time_str.all_after('.') |
| 223 | z := if ms.contains('Z') { 'Z' } else { '' } |
| 224 | ms = ms.replace('Z', '') |
| 225 | if ms.len > 3 { |
| 226 | ms = ms[..3] |
| 227 | } |
| 228 | return date_and_time + '.' + ms + z |
| 229 | } else { |
| 230 | return time_str |
| 231 | } |
| 232 | } |
| 233 | |
| 234 | // to_toml_rs returns an toml_rs compatible json string converted from the `value` ast.Value. |
| 235 | fn to_toml_rs(value ast.Value, array_type int) string { |
| 236 | match value { |
| 237 | ast.Quoted { |
| 238 | json_text := json2.Any(value.text).json_str() |
| 239 | return '{ "type": "string", "value": ${json_text} }' |
| 240 | } |
| 241 | ast.DateTime { |
| 242 | // Normalization for json |
| 243 | mut json_text := json2.Any(value.text).json_str().to_upper().replace(' ', 'T') |
| 244 | typ := if json_text.ends_with('Z"') || json_text.all_after('T').contains('-') |
| 245 | || json_text.all_after('T').contains('+') { |
| 246 | 'datetime' |
| 247 | } else { |
| 248 | 'datetime-local' |
| 249 | } |
| 250 | // NOTE test suite inconsistency. |
| 251 | // It seems it's implementation specific how time and |
| 252 | // date-time values are represented in detail. For now we follow the toml-lang format |
| 253 | // that expands to 6 digits which is also a valid RFC 3339 representation. |
| 254 | json_text = to_toml_rs_time(json_text[1..json_text.len - 1]) |
| 255 | return '{ "type": "${typ}", "value": "${json_text}" }' |
| 256 | } |
| 257 | ast.Date { |
| 258 | json_text := json2.Any(value.text).json_str() |
| 259 | return '{ "type": "date", "value": ${json_text} }' |
| 260 | } |
| 261 | ast.Time { |
| 262 | mut json_text := json2.Any(value.text).json_str() |
| 263 | json_text = to_toml_rs_time(json_text[1..json_text.len - 1]) |
| 264 | return '{ "type": "time", "value": "${json_text}" }' |
| 265 | } |
| 266 | ast.Bool { |
| 267 | json_text := json2.Any(value.text.bool()).json_str() |
| 268 | return '{ "type": "bool", "value": "${json_text}" }' |
| 269 | } |
| 270 | ast.Null { |
| 271 | json_text := json2.Any(value.text).json_str() |
| 272 | return '{ "type": "null", "value": ${json_text} }' |
| 273 | } |
| 274 | ast.Number { |
| 275 | text := value.text |
| 276 | if text.contains('inf') || text.contains('nan') { |
| 277 | return '{ "type": "float", "value": ${value.text} }' |
| 278 | } |
| 279 | if !text.starts_with('0x') && (text.contains('.') || text.to_lower().contains('e')) { |
| 280 | mut val := '' |
| 281 | if text.to_lower().contains('e') && !text.contains('-') { |
| 282 | val = '${value.f64():.1f}' |
| 283 | } else { |
| 284 | val = '${value.f64()}' |
| 285 | } |
| 286 | return '{ "type": "float", "value": "${val}" }' |
| 287 | } |
| 288 | return '{ "type": "integer", "value": "${value.i64()}" }' |
| 289 | } |
| 290 | map[string]ast.Value { |
| 291 | mut str := '{ ' |
| 292 | for key, val in value { |
| 293 | json_key := json2.Any(key).json_str() |
| 294 | str += ' ${json_key}: ${to_toml_rs(val, array_type)},' |
| 295 | } |
| 296 | str = str.trim_right(',') |
| 297 | str += ' }' |
| 298 | return str |
| 299 | } |
| 300 | []ast.Value { |
| 301 | mut str := '' |
| 302 | if array_type == 1 { |
| 303 | str = '{ "type": "array", "value": [ ' |
| 304 | } else { |
| 305 | str = '[ ' |
| 306 | } |
| 307 | for val in value { |
| 308 | str += ' ${to_toml_rs(val, array_type)},' |
| 309 | } |
| 310 | str = str.trim_right(',') |
| 311 | if array_type == 1 { |
| 312 | str += ' ] }\n' |
| 313 | } else { |
| 314 | str += ' ]\n' |
| 315 | } |
| 316 | return str |
| 317 | } |
| 318 | } |
| 319 | |
| 320 | return '<error>' |
| 321 | } |
| 322 | |