v / vlib / toml / tests / toml_lang_test.v
305 lines · 274 sloc · 10.8 KB · 8e35f4d9848f7ad35d857a187dddbfd2eca5e19d
Raw
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
6import os
7import toml
8import toml.ast
9import x.json2
10
11const test_root = os.join_path(@VEXEROOT, 'vlib/toml/tests/testdata/toml_lang/tests')
12const test_files_file = os.join_path(test_root, 'files-toml-1.0.0')
13
14const hide_oks = os.getenv('VTEST_HIDE_OK') == '1'
15const 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.
19const 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.
23const invalid_exceptions = [
24 'do_not_remove',
25]
26const valid_value_exceptions = [
27 'do_not_remove',
28]
29const jq_not_equal = [
30 'do_not_remove',
31]
32
33const jq = os.find_abs_path_of_executable('jq') or { '' }
34const compare_work_dir_root = os.join_path(os.vtmp_dir(), 'toml_toml_lang')
35// From: https://stackoverflow.com/a/38266731/1904615
36const jq_normalize = r'# Apply f to composite entities recursively using keys[], and to atoms
37def 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
46def normalize: sorted_walk(if type == "array" then sort else . end);
47
48normalize'
49
50fn 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.
59fn 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.
226fn 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